pax_global_header00006660000000000000000000000064131345012310014503gustar00rootroot0000000000000052 comment=279d0ca2db52a73798e0c50ccf347f26028f283b docker-registry-2.6.2~ds1/000077500000000000000000000000001313450123100154355ustar00rootroot00000000000000docker-registry-2.6.2~ds1/.gitignore000066400000000000000000000006611313450123100174300ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof # never checkin from the bin file (for now) bin/* # Test key files *.pem # Cover profiles *.out # Editor/IDE specific files. *.sublime-project *.sublime-workspace docker-registry-2.6.2~ds1/.mailmap000066400000000000000000000027241313450123100170630ustar00rootroot00000000000000Stephen J Day Stephen Day Stephen J Day Stephen Day Olivier Gambier Olivier Gambier Brian Bland Brian Bland Brian Bland Brian Bland Josh Hawn Josh Hawn Richard Scothern Richard Richard Scothern Richard Scothern Andrew Meredith Andrew Meredith harche harche Jessie Frazelle Sharif Nassar Sharif Nassar Sven Dowideit Sven Dowideit Vincent Giersch Vincent Giersch davidli davidli Omer Cohen Omer Cohen Eric Yang Eric Yang Nikita Tarasov Nikita Misty Stanley-Jones Misty Stanley-Jones docker-registry-2.6.2~ds1/AUTHORS000066400000000000000000000147701313450123100165160ustar00rootroot00000000000000Aaron Lehmann Aaron Schlesinger Aaron Vinson Adam Duke Adam Enger Adrian Mouat Ahmet Alp Balkan Alex Chan Alex Elman Alexey Gladkov allencloud amitshukla Amy Lindburg Andrew Hsu Andrew Meredith Andrew T Nguyen Andrey Kostov Andy Goldstein Anis Elleuch Antonio Mercado Antonio Murdaca Anton Tiurin Anusha Ragunathan a-palchikov Arien Holthuizen Arnaud Porterie Arthur Baars Asuka Suzuki Avi Miller Ayose Cazorla BadZen Ben Bodenmiller Ben Firshman bin liu Brian Bland burnettk Carson A Cezar Sa Espinola Charles Smith Chris Dillon cuiwei13 cyli Daisuke Fujita Daniel Huhn Darren Shepherd Dave Trombley Dave Tucker David Lawrence davidli David Verhasselt David Xia Dejan Golja Derek McGowan Diogo Mónica DJ Enriquez Donald Huang Doug Davis Edgar Lee Eric Yang Fabio Berchtold Fabio Huser farmerworking Felix Yan Florentin Raud Frank Chen Frederick F. Kautz IV gabriell nascimento Gleb Schukin harche Henri Gomez Hua Wang Hu Keping HuKeping Ian Babrou igayoso Jack Griffin James Findley Jason Freidman Jason Heiss Jeff Nickoloff Jess Frazelle Jessie Frazelle jhaohai Jianqing Wang Jihoon Chung Joao Fernandes John Mulhausen John Starks Jonathan Boulle Jon Johnson Jon Poler Jordan Liggitt Josh Chorlton Josh Hawn Julien Fernandez Keerthan Mala Kelsey Hightower Kenneth Lim Kenny Leung Ke Xu liuchang0812 Liu Hua Li Yi Lloyd Ramey Louis Kottmann Luke Carpenter Marcus Martins Mary Anthony Matt Bentley Matt Duch Matthew Green Matt Moore Matt Robenolt Michael Prokop Michal Minar Michal Minář Mike Brown Miquel Sabaté Misty Stanley-Jones Morgan Bauer moxiegirl Nathan Sullivan nevermosby Nghia Tran Nikita Tarasov Noah Treuhaft Nuutti Kotivuori Oilbeater Olivier Gambier Olivier Jacques Omer Cohen Patrick Devine Phil Estes Philip Misiowiec Pierre-Yves Ritschard Qiao Anran Randy Barlow Richard Scothern Rodolfo Carvalho Rusty Conover Sean Boran Sebastiaan van Stijn Sebastien Coavoux Serge Dubrouski Sharif Nassar Shawn Falkner-Horine Shreyas Karnik Simon Thulbourn spacexnice Spencer Rinehart Stan Hu Stefan Majewsky Stefan Weil Stephen J Day Sungho Moon Sven Dowideit Sylvain Baubeau Ted Reed tgic Thomas Sjögren Tianon Gravi Tibor Vass Tonis Tiigi Tony Holdstock-Brown Trevor Pounds Troels Thomsen Victoria Bialas Victor Vieux Vincent Batts Vincent Demeester Vincent Giersch weiyuan.yl W. Trevor King xg.song xiekeyang Yann ROBERT yaoyao.xyy yixi zhang yuexiao-wang yuzou zhouhaibing089 姜继忠 docker-registry-2.6.2~ds1/BUILDING.md000066400000000000000000000116131313450123100171560ustar00rootroot00000000000000 # Building the registry source ## Use-case This is useful if you intend to actively work on the registry. ### Alternatives Most people should use the [official Registry docker image](https://hub.docker.com/r/library/registry/). People looking for advanced operational use cases might consider rolling their own image with a custom Dockerfile inheriting `FROM registry:2`. OS X users who want to run natively can do so following [the instructions here](https://github.com/docker/docker.github.io/blob/master/registry/recipes/osx-setup-guide.md). ### Gotchas You are expected to know your way around with go & git. If you are a casual user with no development experience, and no preliminary knowledge of go, building from source is probably not a good solution for you. ## Build the development environment The first prerequisite of properly building distribution targets is to have a Go development environment setup. Please follow [How to Write Go Code](https://golang.org/doc/code.html) for proper setup. If done correctly, you should have a GOROOT and GOPATH set in the environment. If a Go development environment is setup, one can use `go get` to install the `registry` command from the current latest: go get github.com/docker/distribution/cmd/registry The above will install the source repository into the `GOPATH`. Now create the directory for the registry data (this might require you to set permissions properly) mkdir -p /var/lib/registry ... or alternatively `export REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/somewhere` if you want to store data into another location. The `registry` binary can then be run with the following: $ $GOPATH/bin/registry --version $GOPATH/bin/registry github.com/docker/distribution v2.0.0-alpha.1+unknown > __NOTE:__ While you do not need to use `go get` to checkout the distribution > project, for these build instructions to work, the project must be checked > out in the correct location in the `GOPATH`. This should almost always be > `$GOPATH/src/github.com/docker/distribution`. The registry can be run with the default config using the following incantation: $ $GOPATH/bin/registry serve $GOPATH/src/github.com/docker/distribution/cmd/registry/config-example.yml INFO[0000] endpoint local-5003 disabled, skipping app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown INFO[0000] endpoint local-8083 disabled, skipping app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown INFO[0000] listening on :5000 app.id=34bbec38-a91a-494a-9a3f-b72f9010081f version=v2.0.0-alpha.1+unknown INFO[0000] debug server listening localhost:5001 If it is working, one should see the above log messages. ### Repeatable Builds For the full development experience, one should `cd` into `$GOPATH/src/github.com/docker/distribution`. From there, the regular `go` commands, such as `go test`, should work per package (please see [Developing](#developing) if they don't work). A `Makefile` has been provided as a convenience to support repeatable builds. Please install the following into `GOPATH` for it to work: go get github.com/tools/godep github.com/golang/lint/golint **TODO(stevvooe):** Add a `make setup` command to Makefile to run this. Have to think about how to interact with Godeps properly. Once these commands are available in the `GOPATH`, run `make` to get a full build: $ make + clean + fmt + vet + lint + build github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar github.com/Sirupsen/logrus github.com/docker/libtrust ... github.com/yvasiyarov/gorelic github.com/docker/distribution/registry/handlers github.com/docker/distribution/cmd/registry + test ... ok github.com/docker/distribution/digest 7.875s ok github.com/docker/distribution/manifest 0.028s ok github.com/docker/distribution/notifications 17.322s ? github.com/docker/distribution/registry [no test files] ok github.com/docker/distribution/registry/api/v2 0.101s ? github.com/docker/distribution/registry/auth [no test files] ok github.com/docker/distribution/registry/auth/silly 0.011s ... + /Users/sday/go/src/github.com/docker/distribution/bin/registry + /Users/sday/go/src/github.com/docker/distribution/bin/registry-api-descriptor-template + binaries The above provides a repeatable build using the contents of the vendored Godeps directory. This includes formatting, vetting, linting, building, testing and generating tagged binaries. We can verify this worked by running the registry binary generated in the "./bin" directory: $ ./bin/registry -version ./bin/registry github.com/docker/distribution v2.0.0-alpha.2-80-g16d8b2c.m ### Optional build tags Optional [build tags](http://golang.org/pkg/go/build/) can be provided using the environment variable `DOCKER_BUILDTAGS`. docker-registry-2.6.2~ds1/CHANGELOG.md000066400000000000000000000075151313450123100172560ustar00rootroot00000000000000# Changelog ## 2.6.1 (2017-04-05) #### Registry - Fix `Forwarded` header handling, revert use of `X-Forwarded-Port` - Use driver `Stat` for registry health check ## 2.6.0 (2017-01-18) #### Storage - S3: fixed bug in delete due to read-after-write inconsistency - S3: allow EC2 IAM roles to be used when authorizing region endpoints - S3: add Object ACL Support - S3: fix delete method's notion of subpaths - S3: use multipart upload API in `Move` method for performance - S3: add v2 signature signing for legacy S3 clones - Swift: add simple heuristic to detect incomplete DLOs during read ops - Swift: support different user and tenant domains - Swift: bulk deletes in chunks - Aliyun OSS: fix delete method's notion of subpaths - Aliyun OSS: optimize data copy after upload finishes - Azure: close leaking response body - Fix storage drivers dropping non-EOF errors when listing repositories - Compare path properly when listing repositories in catalog - Add a foreign layer URL host whitelist - Improve catalog enumerate runtime #### Registry - Export `storage.CreateOptions` in top-level package - Enable notifications to endpoints that use self-signed certificates - Properly validate multi-URL foreign layers - Add control over validation of URLs in pushed manifests - Proxy mode: fix socket leak when pull is cancelled - Tag service: properly handle error responses on HEAD request - Support for custom authentication URL in proxying registry - Add configuration option to disable access logging - Add notification filtering by target media type - Manifest: `References()` returns all children - Honor `X-Forwarded-Port` and Forwarded headers - Reference: Preserve tag and digest in With* functions - Add policy configuration for enforcing repository classes #### Client - Changes the client Tags `All()` method to follow links - Allow registry clients to connect via HTTP2 - Better handling of OAuth errors in client #### Spec - Manifest: clarify relationship between urls and foreign layers - Authorization: add support for repository classes #### Manifest - Override media type returned from `Stat()` for existing manifests - Add plugin mediatype to distribution manifest #### Docs - Document `TOOMANYREQUESTS` error code - Document required Let's Encrypt port - Improve documentation around implementation of OAuth2 - Improve documentation for configuration #### Auth - Add support for registry type in scope - Add support for using v2 ping challenges for v1 - Add leeway to JWT `nbf` and `exp` checking - htpasswd: dynamically parse htpasswd file - Fix missing auth headers with PATCH HTTP request when pushing to default port #### Dockerfile - Update to go1.7 - Reorder Dockerfile steps for better layer caching #### Notes Documentation has moved to the documentation repository at `github.com/docker/docker.github.io/tree/master/registry` The registry is go 1.7 compliant, and passes newer, more restrictive `lint` and `vet` ing. ## 2.5.0 (2016-06-14) #### Storage - Ensure uploads directory is cleaned after upload is committed - Add ability to cap concurrent operations in filesystem driver - S3: Add 'us-gov-west-1' to the valid region list - Swift: Handle ceph not returning Last-Modified header for HEAD requests - Add redirect middleware #### Registry - Add support for blobAccessController middleware - Add support for layers from foreign sources - Remove signature store - Add support for Let's Encrypt - Correct yaml key names in configuration #### Client - Add option to get content digest from manifest get #### Spec - Update the auth spec scope grammar to reflect the fact that hostnames are optionally supported - Clarify API documentation around catalog fetch behavior #### API - Support returning HTTP 429 (Too Many Requests) #### Documentation - Update auth documentation examples to show "expires in" as int #### Docker Image - Use Alpine Linux as base image docker-registry-2.6.2~ds1/CONTRIBUTING.md000066400000000000000000000163011313450123100176670ustar00rootroot00000000000000# Contributing to the registry ## Before reporting an issue... ### If your problem is with... - automated builds - your account on the [Docker Hub](https://hub.docker.com/) - any other [Docker Hub](https://hub.docker.com/) issue Then please do not report your issue here - you should instead report it to [https://support.docker.com](https://support.docker.com) ### If you... - need help setting up your registry - can't figure out something - are not sure what's going on or what your problem is Then please do not open an issue here yet - you should first try one of the following support forums: - irc: #docker-distribution on freenode - mailing-list: or https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution ## Reporting an issue properly By following these simple rules you will get better and faster feedback on your issue. - search the bugtracker for an already reported issue ### If you found an issue that describes your problem: - please read other user comments first, and confirm this is the same issue: a given error condition might be indicative of different problems - you may also find a workaround in the comments - please refrain from adding "same thing here" or "+1" comments - you don't need to comment on an issue to get notified of updates: just hit the "subscribe" button - comment if you have some new, technical and relevant information to add to the case - __DO NOT__ comment on closed issues or merged PRs. If you think you have a related problem, open up a new issue and reference the PR or issue. ### If you have not found an existing issue that describes your problem: 1. create a new issue, with a succinct title that describes your issue: - bad title: "It doesn't work with my docker" - good title: "Private registry push fail: 400 error with E_INVALID_DIGEST" 2. copy the output of: - `docker version` - `docker info` - `docker exec registry -version` 3. copy the command line you used to launch your Registry 4. restart your docker daemon in debug mode (add `-D` to the daemon launch arguments) 5. reproduce your problem and get your docker daemon logs showing the error 6. if relevant, copy your registry logs that show the error 7. provide any relevant detail about your specific Registry configuration (e.g., storage backend used) 8. indicate if you are using an enterprise proxy, Nginx, or anything else between you and your Registry ## Contributing a patch for a known bug, or a small correction You should follow the basic GitHub workflow: 1. fork 2. commit a change 3. make sure the tests pass 4. PR Additionally, you must [sign your commits](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#sign-your-work). It's very simple: - configure your name with git: `git config user.name "Real Name" && git config user.email mail@example.com` - sign your commits using `-s`: `git commit -s -m "My commit"` Some simple rules to ensure quick merge: - clearly point to the issue(s) you want to fix in your PR comment (e.g., `closes #12345`) - prefer multiple (smaller) PRs addressing individual issues over a big one trying to address multiple issues at once - if you need to amend your PR following comments, please squash instead of adding more commits ## Contributing new features You are heavily encouraged to first discuss what you want to do. You can do so on the irc channel, or by opening an issue that clearly describes the use case you want to fulfill, or the problem you are trying to solve. If this is a major new feature, you should then submit a proposal that describes your technical solution and reasoning. If you did discuss it first, this will likely be greenlighted very fast. It's advisable to address all feedback on this proposal before starting actual work. Then you should submit your implementation, clearly linking to the issue (and possible proposal). Your PR will be reviewed by the community, then ultimately by the project maintainers, before being merged. It's mandatory to: - interact respectfully with other community members and maintainers - more generally, you are expected to abide by the [Docker community rules](https://github.com/docker/docker/blob/master/CONTRIBUTING.md#docker-community-guidelines) - address maintainers' comments and modify your submission accordingly - write tests for any new code Complying to these simple rules will greatly accelerate the review process, and will ensure you have a pleasant experience in contributing code to the Registry. Have a look at a great, successful contribution: the [Swift driver PR](https://github.com/docker/distribution/pull/493) ## Coding Style Unless explicitly stated, we follow all coding guidelines from the Go community. While some of these standards may seem arbitrary, they somehow seem to result in a solid, consistent codebase. It is possible that the code base does not currently comply with these guidelines. We are not looking for a massive PR that fixes this, since that goes against the spirit of the guidelines. All new contributions should make a best effort to clean up and make the code base better than they left it. Obviously, apply your best judgement. Remember, the goal here is to make the code base easier for humans to navigate and understand. Always keep that in mind when nudging others to comply. The rules: 1. All code should be formatted with `gofmt -s`. 2. All code should pass the default levels of [`golint`](https://github.com/golang/lint). 3. All code should follow the guidelines covered in [Effective Go](http://golang.org/doc/effective_go.html) and [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments). 4. Comment the code. Tell us the why, the history and the context. 5. Document _all_ declarations and methods, even private ones. Declare expectations, caveats and anything else that may be important. If a type gets exported, having the comments already there will ensure it's ready. 6. Variable name length should be proportional to its context and no longer. `noCommaALongVariableNameLikeThisIsNotMoreClearWhenASimpleCommentWouldDo`. In practice, short methods will have short variable names and globals will have longer names. 7. No underscores in package names. If you need a compound name, step back, and re-examine why you need a compound name. If you still think you need a compound name, lose the underscore. 8. No utils or helpers packages. If a function is not general enough to warrant its own package, it has not been written generally enough to be a part of a util package. Just leave it unexported and well-documented. 9. All tests should run with `go test` and outside tooling should not be required. No, we don't need another unit testing framework. Assertion packages are acceptable if they provide _real_ incremental value. 10. Even though we call these "rules" above, they are actually just guidelines. Since you've read all the rules, you now know that. If you are having trouble getting into the mood of idiomatic Go, we recommend reading through [Effective Go](http://golang.org/doc/effective_go.html). The [Go Blog](http://blog.golang.org/) is also a great resource. Drinking the kool-aid is a lot easier than going thirsty. docker-registry-2.6.2~ds1/Dockerfile000066400000000000000000000006771313450123100174410ustar00rootroot00000000000000FROM golang:1.7-alpine ENV DISTRIBUTION_DIR /go/src/github.com/docker/distribution ENV DOCKER_BUILDTAGS include_oss include_gcs RUN set -ex \ && apk add --no-cache make git WORKDIR $DISTRIBUTION_DIR COPY . $DISTRIBUTION_DIR COPY cmd/registry/config-dev.yml /etc/docker/registry/config.yml RUN make PREFIX=/go clean binaries VOLUME ["/var/lib/registry"] EXPOSE 5000 ENTRYPOINT ["registry"] CMD ["serve", "/etc/docker/registry/config.yml"] docker-registry-2.6.2~ds1/Godeps/000077500000000000000000000000001313450123100166565ustar00rootroot00000000000000docker-registry-2.6.2~ds1/Godeps/Godeps.json000066400000000000000000000316361313450123100210030ustar00rootroot00000000000000{ "ImportPath": "github.com/docker/distribution", "GoVersion": "go1.6", "GodepVersion": "v74", "Packages": [ "./..." ], "Deps": [ { "ImportPath": "github.com/Azure/azure-sdk-for-go/storage", "Comment": "v5.0.0-beta-6-g0b5fe2a", "Rev": "0b5fe2abe0271ba07049eacaa65922d67c319543" }, { "ImportPath": "github.com/Sirupsen/logrus", "Comment": "v0.7.3", "Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d" }, { "ImportPath": "github.com/Sirupsen/logrus/formatters/logstash", "Comment": "v0.7.3", "Rev": "55eb11d21d2a31a3cc93838241d04800f52e823d" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/awserr", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/awsutil", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/client", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/client/metadata", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/corehandlers", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/credentials", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/defaults", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/ec2metadata", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/request", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/session", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/aws/signer/v4", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/endpoints", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/query/queryutil", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/rest", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/restxml", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/private/waiter", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/service/cloudfront/sign", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/service/s3", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/go-ini/ini", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/aws/aws-sdk-go/vendor/github.com/jmespath/go-jmespath", "Comment": "v1.2.4", "Rev": "90dec2183a5f5458ee79cbaf4b8e9ab910bc81a6" }, { "ImportPath": "github.com/bugsnag/bugsnag-go", "Comment": "v1.0.2-5-gb1d1530", "Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274" }, { "ImportPath": "github.com/bugsnag/bugsnag-go/errors", "Comment": "v1.0.2-5-gb1d1530", "Rev": "b1d153021fcd90ca3f080db36bec96dc690fb274" }, { "ImportPath": "github.com/bugsnag/osext", "Rev": "0dd3f918b21bec95ace9dc86c7e70266cfc5c702" }, { "ImportPath": "github.com/bugsnag/panicwrap", "Comment": "1.0.0-2-ge2c2850", "Rev": "e2c28503fcd0675329da73bf48b33404db873782" }, { "ImportPath": "github.com/denverdino/aliyungo/common", "Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2" }, { "ImportPath": "github.com/denverdino/aliyungo/oss", "Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2" }, { "ImportPath": "github.com/denverdino/aliyungo/util", "Rev": "afedced274aa9a7fcdd47ac97018f0f8db4e5de2" }, { "ImportPath": "github.com/docker/goamz/aws", "Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85" }, { "ImportPath": "github.com/docker/goamz/s3", "Rev": "f0a21f5b2e12f83a505ecf79b633bb2035cf6f85" }, { "ImportPath": "github.com/docker/libtrust", "Rev": "fa567046d9b14f6aa788882a950d69651d230b21" }, { "ImportPath": "github.com/garyburd/redigo/internal", "Rev": "535138d7bcd717d6531c701ef5933d98b1866257" }, { "ImportPath": "github.com/garyburd/redigo/redis", "Rev": "535138d7bcd717d6531c701ef5933d98b1866257" }, { "ImportPath": "github.com/golang/protobuf/proto", "Rev": "8d92cf5fc15a4382f8964b08e1f42a75c0591aa3" }, { "ImportPath": "github.com/gorilla/context", "Rev": "14f550f51af52180c2eefed15e5fd18d63c0a64a" }, { "ImportPath": "github.com/gorilla/handlers", "Rev": "60c7bfde3e33c201519a200a4507a158cc03a17b" }, { "ImportPath": "github.com/gorilla/mux", "Rev": "e444e69cbd2e2e3e0749a2f3c717cec491552bbf" }, { "ImportPath": "github.com/inconshreveable/mousetrap", "Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" }, { "ImportPath": "github.com/mitchellh/mapstructure", "Rev": "482a9fd5fa83e8c4e7817413b80f3eb8feec03ef" }, { "ImportPath": "github.com/ncw/swift", "Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86" }, { "ImportPath": "github.com/ncw/swift/swifttest", "Rev": "ce444d6d47c51d4dda9202cd38f5094dd8e27e86" }, { "ImportPath": "github.com/spf13/cobra", "Rev": "312092086bed4968099259622145a0c9ae280064" }, { "ImportPath": "github.com/spf13/pflag", "Rev": "5644820622454e71517561946e3d94b9f9db6842" }, { "ImportPath": "github.com/stevvooe/resumable", "Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4" }, { "ImportPath": "github.com/stevvooe/resumable/sha256", "Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4" }, { "ImportPath": "github.com/stevvooe/resumable/sha512", "Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4" }, { "ImportPath": "github.com/yvasiyarov/go-metrics", "Rev": "57bccd1ccd43f94bb17fdd8bf3007059b802f85e" }, { "ImportPath": "github.com/yvasiyarov/gorelic", "Comment": "v0.0.6-8-ga9bba5b", "Rev": "a9bba5b9ab508a086f9a12b8c51fab68478e2128" }, { "ImportPath": "github.com/yvasiyarov/newrelic_platform_go", "Rev": "b21fdbd4370f3717f3bbd2bf41c223bc273068e6" }, { "ImportPath": "golang.org/x/crypto/bcrypt", "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" }, { "ImportPath": "golang.org/x/crypto/blowfish", "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" }, { "ImportPath": "golang.org/x/crypto/ocsp", "Rev": "c10c31b5e94b6f7a0283272dc2bb27163dcea24b" }, { "ImportPath": "golang.org/x/net/context", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/net/context/ctxhttp", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/net/http2", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/net/http2/hpack", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/net/internal/timeseries", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/net/trace", "Rev": "4876518f9e71663000c348837735820161a42df7" }, { "ImportPath": "golang.org/x/oauth2", "Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf" }, { "ImportPath": "golang.org/x/oauth2/google", "Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf" }, { "ImportPath": "golang.org/x/oauth2/internal", "Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf" }, { "ImportPath": "golang.org/x/oauth2/jws", "Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf" }, { "ImportPath": "golang.org/x/oauth2/jwt", "Rev": "045497edb6234273d67dbc25da3f2ddbc4c4cacf" }, { "ImportPath": "golang.org/x/time/rate", "Rev": "a4bde12657593d5e90d0533a3e4fd95e635124cb" }, { "ImportPath": "google.golang.org/api/gensupport", "Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54" }, { "ImportPath": "google.golang.org/api/googleapi", "Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54" }, { "ImportPath": "google.golang.org/api/googleapi/internal/uritemplates", "Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54" }, { "ImportPath": "google.golang.org/api/storage/v1", "Rev": "9bf6e6e569ff057f75d9604a46c52928f17d2b54" }, { "ImportPath": "google.golang.org/appengine", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/app_identity", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/base", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/datastore", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/log", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/modules", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/appengine/internal/remote_api", "Rev": "12d5545dc1cfa6047a286d5e853841b6471f4c19" }, { "ImportPath": "google.golang.org/cloud", "Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2" }, { "ImportPath": "google.golang.org/cloud/compute/metadata", "Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2" }, { "ImportPath": "google.golang.org/cloud/internal", "Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2" }, { "ImportPath": "google.golang.org/cloud/internal/opts", "Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2" }, { "ImportPath": "google.golang.org/cloud/storage", "Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2" }, { "ImportPath": "google.golang.org/grpc", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/codes", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/credentials", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/grpclog", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/internal", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/metadata", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/naming", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/peer", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "google.golang.org/grpc/transport", "Rev": "d3ddb4469d5a1b949fc7a7da7c1d6a0d1b6de994" }, { "ImportPath": "gopkg.in/check.v1", "Rev": "64131543e7896d5bcc6bd5a76287eb75ea96c673" }, { "ImportPath": "gopkg.in/yaml.v2", "Rev": "bef53efd0c76e49e6de55ead051f886bea7e9420" }, { "ImportPath": "rsc.io/letsencrypt", "Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c" }, { "ImportPath": "rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme", "Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c" }, { "ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1", "Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c" }, { "ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher", "Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c" }, { "ImportPath": "rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json", "Rev": "a019c9e6fce0c7132679dea13bd8df7c86ffe26c" } ] } docker-registry-2.6.2~ds1/Godeps/Readme000066400000000000000000000002101313450123100177670ustar00rootroot00000000000000This directory tree is generated automatically by godep. Please do not edit. See https://github.com/tools/godep for more information. docker-registry-2.6.2~ds1/LICENSE000066400000000000000000000260751313450123100164540ustar00rootroot00000000000000Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. docker-registry-2.6.2~ds1/MAINTAINERS000066400000000000000000000025261313450123100171370ustar00rootroot00000000000000# Distribution maintainers file # # This file describes who runs the docker/distribution project and how. # This is a living document - if you see something out of date or missing, speak up! # # It is structured to be consumable by both humans and programs. # To extract its contents programmatically, use any TOML-compliant parser. # # This file is compiled into the MAINTAINERS file in docker/opensource. # [Org] [Org."Core maintainers"] people = [ "aaronlehmann", "dmcgowan", "dmp42", "richardscothern", "shykes", "stevvooe", ] [people] # A reference list of all people associated with the project. # All other sections should refer to people by their canonical key # in the people section. # ADD YOURSELF HERE IN ALPHABETICAL ORDER [people.aaronlehmann] Name = "Aaron Lehmann" Email = "aaron.lehmann@docker.com" GitHub = "aaronlehmann" [people.dmcgowan] Name = "Derek McGowan" Email = "derek@mcgstyle.net" GitHub = "dmcgowan" [people.dmp42] Name = "Olivier Gambier" Email = "olivier@docker.com" GitHub = "dmp42" [people.richardscothern] Name = "Richard Scothern" Email = "richard.scothern@gmail.com" GitHub = "richardscothern" [people.shykes] Name = "Solomon Hykes" Email = "solomon@docker.com" GitHub = "shykes" [people.stevvooe] Name = "Stephen Day" Email = "stephen.day@docker.com" GitHub = "stevvooe" docker-registry-2.6.2~ds1/Makefile000066400000000000000000000062471313450123100171060ustar00rootroot00000000000000# Set an output prefix, which is the local directory if not specified PREFIX?=$(shell pwd) # Used to populate version variable in main package. VERSION=$(shell git describe --match 'v[0-9]*' --dirty='.m' --always) # Allow turning off function inlining and variable registerization ifeq (${DISABLE_OPTIMIZATION},true) GO_GCFLAGS=-gcflags "-N -l" VERSION:="$(VERSION)-noopt" endif GO_LDFLAGS=-ldflags "-X `go list ./version`.Version=$(VERSION)" .PHONY: all build binaries clean dep-restore dep-save dep-validate fmt lint test test-full vet .DEFAULT: all all: fmt vet lint build test binaries AUTHORS: .mailmap .git/HEAD git log --format='%aN <%aE>' | sort -fu > $@ # This only needs to be generated by hand when cutting full releases. version/version.go: ./version/version.sh > $@ # Required for go 1.5 to build GO15VENDOREXPERIMENT := 1 # Go files GOFILES=$(shell find . -type f -name '*.go') # Package list PKGS=$(shell go list -tags "${DOCKER_BUILDTAGS}" ./... | grep -v ^github.com/docker/distribution/vendor/) # Resolving binary dependencies for specific targets GOLINT=$(shell which golint || echo '') GODEP=$(shell which godep || echo '') ${PREFIX}/bin/registry: $(GOFILES) @echo "+ $@" @go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry ${PREFIX}/bin/digest: $(GOFILES) @echo "+ $@" @go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/digest ${PREFIX}/bin/registry-api-descriptor-template: $(GOFILES) @echo "+ $@" @go build -o $@ ${GO_LDFLAGS} ${GO_GCFLAGS} ./cmd/registry-api-descriptor-template docs/spec/api.md: docs/spec/api.md.tmpl ${PREFIX}/bin/registry-api-descriptor-template ./bin/registry-api-descriptor-template $< > $@ vet: @echo "+ $@" @go vet -tags "${DOCKER_BUILDTAGS}" $(PKGS) fmt: @echo "+ $@" @test -z "$$(gofmt -s -l . 2>&1 | grep -v ^vendor/ | tee /dev/stderr)" || \ (echo >&2 "+ please format Go code with 'gofmt -s'" && false) lint: @echo "+ $@" $(if $(GOLINT), , \ $(error Please install golint: `go get -u github.com/golang/lint/golint`)) @test -z "$$($(GOLINT) ./... 2>&1 | grep -v ^vendor/ | tee /dev/stderr)" build: @echo "+ $@" @go build -tags "${DOCKER_BUILDTAGS}" -v ${GO_LDFLAGS} $(PKGS) test: @echo "+ $@" @go test -test.short -tags "${DOCKER_BUILDTAGS}" $(PKGS) test-full: @echo "+ $@" @go test -tags "${DOCKER_BUILDTAGS}" $(PKGS) binaries: ${PREFIX}/bin/registry ${PREFIX}/bin/digest ${PREFIX}/bin/registry-api-descriptor-template @echo "+ $@" clean: @echo "+ $@" @rm -rf "${PREFIX}/bin/registry" "${PREFIX}/bin/digest" "${PREFIX}/bin/registry-api-descriptor-template" dep-save: @echo "+ $@" $(if $(GODEP), , \ $(error Please install godep: go get github.com/tools/godep)) @$(GODEP) save $(PKGS) dep-restore: @echo "+ $@" $(if $(GODEP), , \ $(error Please install godep: go get github.com/tools/godep)) @$(GODEP) restore -v dep-validate: dep-restore @echo "+ $@" @rm -Rf .vendor.bak @mv vendor .vendor.bak @rm -Rf Godeps @$(GODEP) save ./... @test -z "$$(diff -r vendor .vendor.bak 2>&1 | tee /dev/stderr)" || \ (echo >&2 "+ borked dependencies! what you have in Godeps/Godeps.json does not match with what you have in vendor" && false) @rm -Rf .vendor.bak docker-registry-2.6.2~ds1/README.md000066400000000000000000000126301313450123100167160ustar00rootroot00000000000000# Distribution The Docker toolset to pack, ship, store, and deliver content. This repository's main product is the Docker Registry 2.0 implementation for storing and distributing Docker images. It supersedes the [docker/docker-registry](https://github.com/docker/docker-registry) project with a new API design, focused around security and performance. [![Circle CI](https://circleci.com/gh/docker/distribution/tree/master.svg?style=svg)](https://circleci.com/gh/docker/distribution/tree/master) [![GoDoc](https://godoc.org/github.com/docker/distribution?status.svg)](https://godoc.org/github.com/docker/distribution) This repository contains the following components: |**Component** |Description | |--------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | **registry** | An implementation of the [Docker Registry HTTP API V2](docs/spec/api.md) for use with docker 1.6+. | | **libraries** | A rich set of libraries for interacting with distribution components. Please see [godoc](https://godoc.org/github.com/docker/distribution) for details. **Note**: These libraries are **unstable**. | | **specifications** | _Distribution_ related specifications are available in [docs/spec](docs/spec) | | **documentation** | Docker's full documentation set is available at [docs.docker.com](https://docs.docker.com). This repository [contains the subset](docs/) related just to the registry. | ### How does this integrate with Docker engine? This project should provide an implementation to a V2 API for use in the [Docker core project](https://github.com/docker/docker). The API should be embeddable and simplify the process of securely pulling and pushing content from `docker` daemons. ### What are the long term goals of the Distribution project? The _Distribution_ project has the further long term goal of providing a secure tool chain for distributing content. The specifications, APIs and tools should be as useful with Docker as they are without. Our goal is to design a professional grade and extensible content distribution system that allow users to: * Enjoy an efficient, secured and reliable way to store, manage, package and exchange content * Hack/roll their own on top of healthy open-source components * Implement their own home made solution through good specs, and solid extensions mechanism. ## More about Registry 2.0 The new registry implementation provides the following benefits: - faster push and pull - new, more efficient implementation - simplified deployment - pluggable storage backend - webhook notifications For information on upcoming functionality, please see [ROADMAP.md](ROADMAP.md). ### Who needs to deploy a registry? By default, Docker users pull images from Docker's public registry instance. [Installing Docker](https://docs.docker.com/engine/installation/) gives users this ability. Users can also push images to a repository on Docker's public registry, if they have a [Docker Hub](https://hub.docker.com/) account. For some users and even companies, this default behavior is sufficient. For others, it is not. For example, users with their own software products may want to maintain a registry for private, company images. Also, you may wish to deploy your own image repository for images used to test or in continuous integration. For these use cases and others, [deploying your own registry instance](https://github.com/docker/docker.github.io/blob/master/registry/deploying.md) may be the better choice. ### Migration to Registry 2.0 For those who have previously deployed their own registry based on the Registry 1.0 implementation and wish to deploy a Registry 2.0 while retaining images, data migration is required. A tool to assist with migration efforts has been created. For more information see [docker/migrator] (https://github.com/docker/migrator). ## Contribute Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to contribute issues, fixes, and patches to this project. If you are contributing code, see the instructions for [building a development environment](BUILDING.md). ## Support If any issues are encountered while using the _Distribution_ project, several avenues are available for support:
IRC #docker-distribution on FreeNode
Issue Tracker github.com/docker/distribution/issues
Google Groups https://groups.google.com/a/dockerproject.org/forum/#!forum/distribution
Mailing List docker@dockerproject.org
## License This project is distributed under [Apache License, Version 2.0](LICENSE). docker-registry-2.6.2~ds1/RELEASE-CHECKLIST.md000066400000000000000000000031221313450123100203440ustar00rootroot00000000000000## Registry Release Checklist 10. Compile release notes detailing features and since the last release. Update the `CHANGELOG.md` file. 20. Update the version file: `https://github.com/docker/distribution/blob/master/version/version.go` 30. Update the `MAINTAINERS` (if necessary), `AUTHORS` and `.mailmap` files. ``` make AUTHORS ``` 40. Create a signed tag. Distribution uses semantic versioning. Tags are of the format `vx.y.z[-rcn]` You will need PGP installed and a PGP key which has been added to your Github account. The comment for the tag should include the release notes. 50. Push the signed tag 60. Create a new [release](https://github.com/docker/distribution/releases). In the case of a release candidate, tick the `pre-release` checkbox. 70. Update the registry binary in [distribution library image repo](https://github.com/docker/distribution-library-image) by running the update script and opening a pull request. 80. Update the official image. Add the new version in the [official images repo](https://github.com/docker-library/official-images) by appending a new version to the `registry/registry` file with the git hash pointed to by the signed tag. Update the major version to point to the latest version and the minor version to point to new patch release if necessary. e.g. to release `2.3.1` `2.3.1 (new)` `2.3.0 -> 2.3.0` can be removed `2 -> 2.3.1` `2.3 -> 2.3.1` 90. Build a new distribution/registry image on [Docker hub](https://hub.docker.com/u/distribution/dashboard) by adding a new automated build with the new tag and re-building the images. docker-registry-2.6.2~ds1/ROADMAP.md000066400000000000000000000321771313450123100170540ustar00rootroot00000000000000# Roadmap The Distribution Project consists of several components, some of which are still being defined. This document defines the high-level goals of the project, identifies the current components, and defines the release- relationship to the Docker Platform. * [Distribution Goals](#distribution-goals) * [Distribution Components](#distribution-components) * [Project Planning](#project-planning): release-relationship to the Docker Platform. This road map is a living document, providing an overview of the goals and considerations made in respect of the future of the project. ## Distribution Goals - Replace the existing [docker registry](github.com/docker/docker-registry) implementation as the primary implementation. - Replace the existing push and pull code in the docker engine with the distribution package. - Define a strong data model for distributing docker images - Provide a flexible distribution tool kit for use in the docker platform - Unlock new distribution models ## Distribution Components Components of the Distribution Project are managed via github [milestones](https://github.com/docker/distribution/milestones). Upcoming features and bugfixes for a component will be added to the relevant milestone. If a feature or bugfix is not part of a milestone, it is currently unscheduled for implementation. * [Registry](#registry) * [Distribution Package](#distribution-package) *** ### Registry The new Docker registry is the main portion of the distribution repository. Registry 2.0 is the first release of the next-generation registry. This was primarily focused on implementing the [new registry API](https://github.com/docker/distribution/blob/master/docs/spec/api.md), with a focus on security and performance. Following from the Distribution project goals above, we have a set of goals for registry v2 that we would like to follow in the design. New features should be compared against these goals. #### Data Storage and Distribution First The registry's first goal is to provide a reliable, consistent storage location for Docker images. The registry should only provide the minimal amount of indexing required to fetch image data and no more. This means we should be selective in new features and API additions, including those that may require expensive, ever growing indexes. Requests should be servable in "constant time". #### Content Addressability All data objects used in the registry API should be content addressable. Content identifiers should be secure and verifiable. This provides a secure, reliable base from which to build more advanced content distribution systems. #### Content Agnostic In the past, changes to the image format would require large changes in Docker and the Registry. By decoupling the distribution and image format, we can allow the formats to progress without having to coordinate between the two. This means that we should be focused on decoupling Docker from the registry just as much as decoupling the registry from Docker. Such an approach will allow us to unlock new distribution models that haven't been possible before. We can take this further by saying that the new registry should be content agnostic. The registry provides a model of names, tags, manifests and content addresses and that model can be used to work with content. #### Simplicity The new registry should be closer to a microservice component than its predecessor. This means it should have a narrower API and a low number of service dependencies. It should be easy to deploy. This means that other solutions should be explored before changing the API or adding extra dependencies. If functionality is required, can it be added as an extension or companion service. #### Extensibility The registry should provide extension points to add functionality. By keeping the scope narrow, but providing the ability to add functionality. Features like search, indexing, synchronization and registry explorers fall into this category. No such feature should be added unless we've found it impossible to do through an extension. #### Active Feature Discussions The following are feature discussions that are currently active. If you don't see your favorite, unimplemented feature, feel free to contact us via IRC or the mailing list and we can talk about adding it. The goal here is to make sure that new features go through a rigid design process before landing in the registry. ##### Proxying to other Registries A _pull-through caching_ mode exists for the registry, but is restricted from within the docker client to only mirror the official Docker Hub. This functionality can be expanded when image provenance has been specified and implemented in the distribution project. ##### Metadata storage Metadata for the registry is currently stored with the manifest and layer data on the storage backend. While this is a big win for simplicity and reliably maintaining state, it comes with the cost of consistency and high latency. The mutable registry metadata operations should be abstracted behind an API which will allow ACID compliant storage systems to handle metadata. ##### Peer to Peer transfer Discussion has started here: https://docs.google.com/document/d/1rYDpSpJiQWmCQy8Cuiaa3NH-Co33oK_SC9HeXYo87QA/edit ##### Indexing, Search and Discovery The original registry provided some implementation of search for use with private registries. Support has been elided from V2 since we'd like to both decouple search functionality from the registry. The makes the registry simpler to deploy, especially in use cases where search is not needed, and let's us decouple the image format from the registry. There are explorations into using the catalog API and notification system to build external indexes. The current line of thought is that we will define a common search API to index and query docker images. Such a system could be run as a companion to a registry or set of registries to power discovery. The main issue with search and discovery is that there are so many ways to accomplish it. There are two aspects to this project. The first is deciding on how it will be done, including an API definition that can work with changing data formats. The second is the process of integrating with `docker search`. We expect that someone attempts to address the problem with the existing tools and propose it as a standard search API or uses it to inform a standardization process. Once this has been explored, we integrate with the docker client. Please see the following for more detail: - https://github.com/docker/distribution/issues/206 ##### Deletes > __NOTE:__ Deletes are a much asked for feature. Before requesting this feature or participating in discussion, we ask that you read this section in full and understand the problems behind deletes. While, at first glance, implementing deleting seems simple, there are a number mitigating factors that make many solutions not ideal or even pathological in the context of a registry. The following paragraph discuss the background and approaches that could be applied to arrive at a solution. The goal of deletes in any system is to remove unused or unneeded data. Only data requested for deletion should be removed and no other data. Removing unintended data is worse than _not_ removing data that was requested for removal but ideally, both are supported. Generally, according to this rule, we err on holding data longer than needed, ensuring that it is only removed when we can be certain that it can be removed. With the current behavior, we opt to hold onto the data forever, ensuring that data cannot be incorrectly removed. To understand the problems with implementing deletes, one must understand the data model. All registry data is stored in a filesystem layout, implemented on a "storage driver", effectively a _virtual file system_ (VFS). The storage system must assume that this VFS layer will be eventually consistent and has poor read- after-write consistency, since this is the lower common denominator among the storage drivers. This is mitigated by writing values in reverse- dependent order, but makes wider transactional operations unsafe. Layered on the VFS model is a content-addressable _directed, acyclic graph_ (DAG) made up of blobs. Manifests reference layers. Tags reference manifests. Since the same data can be referenced by multiple manifests, we only store data once, even if it is in different repositories. Thus, we have a set of blobs, referenced by tags and manifests. If we want to delete a blob we need to be certain that it is no longer referenced by another manifest or tag. When we delete a manifest, we also can try to delete the referenced blobs. Deciding whether or not a blob has an active reference is the crux of the problem. Conceptually, deleting a manifest and its resources is quite simple. Just find all the manifests, enumerate the referenced blobs and delete the blobs not in that set. An astute observer will recognize this as a garbage collection problem. As with garbage collection in programming languages, this is very simple when one always has a consistent view. When one adds parallelism and an inconsistent view of data, it becomes very challenging. A simple example can demonstrate this. Let's say we are deleting a manifest _A_ in one process. We scan the manifest and decide that all the blobs are ready for deletion. Concurrently, we have another process accepting a new manifest _B_ referencing one or more blobs from the manifest _A_. Manifest _B_ is accepted and all the blobs are considered present, so the operation proceeds. The original process then deletes the referenced blobs, assuming they were unreferenced. The manifest _B_, which we thought had all of its data present, can no longer be served by the registry, since the dependent data has been deleted. Deleting data from the registry safely requires some way to coordinate this operation. The following approaches are being considered: - _Reference Counting_ - Maintain a count of references to each blob. This is challenging for a number of reasons: 1. maintaining a consistent consensus of reference counts across a set of Registries and 2. Building the initial list of reference counts for an existing registry. These challenges can be met with a consensus protocol like Paxos or Raft in the first case and a necessary but simple scan in the second.. - _Lock the World GC_ - Halt all writes to the data store. Walk the data store and find all blob references. Delete all unreferenced blobs. This approach is very simple but requires disabling writes for a period of time while the service reads all data. This is slow and expensive but very accurate and effective. - _Generational GC_ - Do something similar to above but instead of blocking writes, writes are sent to another storage backend while reads are broadcast to the new and old backends. GC is then performed on the read-only portion. Because writes land in the new backend, the data in the read-only section can be safely deleted. The main drawbacks of this approach are complexity and coordination. - _Centralized Oracle_ - Using a centralized, transactional database, we can know exactly which data is referenced at any given time. This avoids coordination problem by managing this data in a single location. We trade off metadata scalability for simplicity and performance. This is a very good option for most registry deployments. This would create a bottleneck for registry metadata. However, metadata is generally not the main bottleneck when serving images. Please let us know if other solutions exist that we have yet to enumerate. Note that for any approach, implementation is a massive consideration. For example, a mark-sweep based solution may seem simple but the amount of work in coordination offset the extra work it might take to build a _Centralized Oracle_. We'll accept proposals for any solution but please coordinate with us before dropping code. At this time, we have traded off simplicity and ease of deployment for disk space. Simplicity and ease of deployment tend to reduce developer involvement, which is currently the most expensive resource in software engineering. Taking on any solution for deletes will greatly effect these factors, trading off very cheap disk space for a complex deployment and operational story. Please see the following issues for more detail: - https://github.com/docker/distribution/issues/422 - https://github.com/docker/distribution/issues/461 - https://github.com/docker/distribution/issues/462 ### Distribution Package At its core, the Distribution Project is a set of Go packages that make up Distribution Components. At this time, most of these packages make up the Registry implementation. The package itself is considered unstable. If you're using it, please take care to vendor the dependent version. For feature additions, please see the Registry section. In the future, we may break out a separate Roadmap for distribution-specific features that apply to more than just the registry. *** ### Project Planning An [Open-Source Planning Process](https://github.com/docker/distribution/wiki/Open-Source-Planning-Process) is used to define the Roadmap. [Project Pages](https://github.com/docker/distribution/wiki) define the goals for each Milestone and identify current progress. docker-registry-2.6.2~ds1/blobs.go000066400000000000000000000226421313450123100170730ustar00rootroot00000000000000package distribution import ( "errors" "fmt" "io" "net/http" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" ) var ( // ErrBlobExists returned when blob already exists ErrBlobExists = errors.New("blob exists") // ErrBlobDigestUnsupported when blob digest is an unsupported version. ErrBlobDigestUnsupported = errors.New("unsupported blob digest") // ErrBlobUnknown when blob is not found. ErrBlobUnknown = errors.New("unknown blob") // ErrBlobUploadUnknown returned when upload is not found. ErrBlobUploadUnknown = errors.New("blob upload unknown") // ErrBlobInvalidLength returned when the blob has an expected length on // commit, meaning mismatched with the descriptor or an invalid value. ErrBlobInvalidLength = errors.New("blob invalid length") ) // ErrBlobInvalidDigest returned when digest check fails. type ErrBlobInvalidDigest struct { Digest digest.Digest Reason error } func (err ErrBlobInvalidDigest) Error() string { return fmt.Sprintf("invalid digest for referenced layer: %v, %v", err.Digest, err.Reason) } // ErrBlobMounted returned when a blob is mounted from another repository // instead of initiating an upload session. type ErrBlobMounted struct { From reference.Canonical Descriptor Descriptor } func (err ErrBlobMounted) Error() string { return fmt.Sprintf("blob mounted from: %v to: %v", err.From, err.Descriptor) } // Descriptor describes targeted content. Used in conjunction with a blob // store, a descriptor can be used to fetch, store and target any kind of // blob. The struct also describes the wire protocol format. Fields should // only be added but never changed. type Descriptor struct { // MediaType describe the type of the content. All text based formats are // encoded as utf-8. MediaType string `json:"mediaType,omitempty"` // Size in bytes of content. Size int64 `json:"size,omitempty"` // Digest uniquely identifies the content. A byte stream can be verified // against against this digest. Digest digest.Digest `json:"digest,omitempty"` // URLs contains the source URLs of this content. URLs []string `json:"urls,omitempty"` // NOTE: Before adding a field here, please ensure that all // other options have been exhausted. Much of the type relationships // depend on the simplicity of this type. } // Descriptor returns the descriptor, to make it satisfy the Describable // interface. Note that implementations of Describable are generally objects // which can be described, not simply descriptors; this exception is in place // to make it more convenient to pass actual descriptors to functions that // expect Describable objects. func (d Descriptor) Descriptor() Descriptor { return d } // BlobStatter makes blob descriptors available by digest. The service may // provide a descriptor of a different digest if the provided digest is not // canonical. type BlobStatter interface { // Stat provides metadata about a blob identified by the digest. If the // blob is unknown to the describer, ErrBlobUnknown will be returned. Stat(ctx context.Context, dgst digest.Digest) (Descriptor, error) } // BlobDeleter enables deleting blobs from storage. type BlobDeleter interface { Delete(ctx context.Context, dgst digest.Digest) error } // BlobEnumerator enables iterating over blobs from storage type BlobEnumerator interface { Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error } // BlobDescriptorService manages metadata about a blob by digest. Most // implementations will not expose such an interface explicitly. Such mappings // should be maintained by interacting with the BlobIngester. Hence, this is // left off of BlobService and BlobStore. type BlobDescriptorService interface { BlobStatter // SetDescriptor assigns the descriptor to the digest. The provided digest and // the digest in the descriptor must map to identical content but they may // differ on their algorithm. The descriptor must have the canonical // digest of the content and the digest algorithm must match the // annotators canonical algorithm. // // Such a facility can be used to map blobs between digest domains, with // the restriction that the algorithm of the descriptor must match the // canonical algorithm (ie sha256) of the annotator. SetDescriptor(ctx context.Context, dgst digest.Digest, desc Descriptor) error // Clear enables descriptors to be unlinked Clear(ctx context.Context, dgst digest.Digest) error } // BlobDescriptorServiceFactory creates middleware for BlobDescriptorService. type BlobDescriptorServiceFactory interface { BlobAccessController(svc BlobDescriptorService) BlobDescriptorService } // ReadSeekCloser is the primary reader type for blob data, combining // io.ReadSeeker with io.Closer. type ReadSeekCloser interface { io.ReadSeeker io.Closer } // BlobProvider describes operations for getting blob data. type BlobProvider interface { // Get returns the entire blob identified by digest along with the descriptor. Get(ctx context.Context, dgst digest.Digest) ([]byte, error) // Open provides a ReadSeekCloser to the blob identified by the provided // descriptor. If the blob is not known to the service, an error will be // returned. Open(ctx context.Context, dgst digest.Digest) (ReadSeekCloser, error) } // BlobServer can serve blobs via http. type BlobServer interface { // ServeBlob attempts to serve the blob, identifed by dgst, via http. The // service may decide to redirect the client elsewhere or serve the data // directly. // // This handler only issues successful responses, such as 2xx or 3xx, // meaning it serves data or issues a redirect. If the blob is not // available, an error will be returned and the caller may still issue a // response. // // The implementation may serve the same blob from a different digest // domain. The appropriate headers will be set for the blob, unless they // have already been set by the caller. ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error } // BlobIngester ingests blob data. type BlobIngester interface { // Put inserts the content p into the blob service, returning a descriptor // or an error. Put(ctx context.Context, mediaType string, p []byte) (Descriptor, error) // Create allocates a new blob writer to add a blob to this service. The // returned handle can be written to and later resumed using an opaque // identifier. With this approach, one can Close and Resume a BlobWriter // multiple times until the BlobWriter is committed or cancelled. Create(ctx context.Context, options ...BlobCreateOption) (BlobWriter, error) // Resume attempts to resume a write to a blob, identified by an id. Resume(ctx context.Context, id string) (BlobWriter, error) } // BlobCreateOption is a general extensible function argument for blob creation // methods. A BlobIngester may choose to honor any or none of the given // BlobCreateOptions, which can be specific to the implementation of the // BlobIngester receiving them. // TODO (brianbland): unify this with ManifestServiceOption in the future type BlobCreateOption interface { Apply(interface{}) error } // CreateOptions is a collection of blob creation modifiers relevant to general // blob storage intended to be configured by the BlobCreateOption.Apply method. type CreateOptions struct { Mount struct { ShouldMount bool From reference.Canonical // Stat allows to pass precalculated descriptor to link and return. // Blob access check will be skipped if set. Stat *Descriptor } } // BlobWriter provides a handle for inserting data into a blob store. // Instances should be obtained from BlobWriteService.Writer and // BlobWriteService.Resume. If supported by the store, a writer can be // recovered with the id. type BlobWriter interface { io.WriteCloser io.ReaderFrom // Size returns the number of bytes written to this blob. Size() int64 // ID returns the identifier for this writer. The ID can be used with the // Blob service to later resume the write. ID() string // StartedAt returns the time this blob write was started. StartedAt() time.Time // Commit completes the blob writer process. The content is verified // against the provided provisional descriptor, which may result in an // error. Depending on the implementation, written data may be validated // against the provisional descriptor fields. If MediaType is not present, // the implementation may reject the commit or assign "application/octet- // stream" to the blob. The returned descriptor may have a different // digest depending on the blob store, referred to as the canonical // descriptor. Commit(ctx context.Context, provisional Descriptor) (canonical Descriptor, err error) // Cancel ends the blob write without storing any data and frees any // associated resources. Any data written thus far will be lost. Cancel // implementations should allow multiple calls even after a commit that // result in a no-op. This allows use of Cancel in a defer statement, // increasing the assurance that it is correctly called. Cancel(ctx context.Context) error } // BlobService combines the operations to access, read and write blobs. This // can be used to describe remote blob services. type BlobService interface { BlobStatter BlobProvider BlobIngester } // BlobStore represent the entire suite of blob related operations. Such an // implementation can access, read, write, delete and serve blobs. type BlobStore interface { BlobService BlobServer BlobDeleter } docker-registry-2.6.2~ds1/circle.yml000066400000000000000000000063411313450123100174250ustar00rootroot00000000000000# Pony-up! machine: pre: # Install gvm - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer) # Install codecov for coverage - pip install --user codecov post: # go - gvm install go1.7 --prefer-binary --name=stable environment: # Convenient shortcuts to "common" locations CHECKOUT: /home/ubuntu/$CIRCLE_PROJECT_REPONAME BASE_DIR: src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME # Trick circle brainflat "no absolute path" behavior BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR DOCKER_BUILDTAGS: "include_oss include_gcs" # Workaround Circle parsing dumb bugs and/or YAML wonkyness CIRCLE_PAIN: "mode: set" hosts: # Not used yet fancy: 127.0.0.1 dependencies: pre: # Copy the code to the gopath of all go versions - > gvm use stable && mkdir -p "$(dirname $BASE_STABLE)" && cp -R "$CHECKOUT" "$BASE_STABLE" override: # Install dependencies for every copied clone/go version - gvm use stable && go get github.com/tools/godep: pwd: $BASE_STABLE post: # For the stable go version, additionally install linting tools - > gvm use stable && go get github.com/axw/gocov/gocov github.com/golang/lint/golint test: pre: # Output the go versions we are going to test # - gvm use old && go version - gvm use stable && go version # todo(richard): replace with a more robust vendoring solution. Removed due to a fundamental disagreement in godep philosophies. # Ensure validation of dependencies # - gvm use stable && if test -n "`git diff --stat=1000 master | grep -Ei \"vendor|godeps\"`"; then make dep-validate; fi: # pwd: $BASE_STABLE # First thing: build everything. This will catch compile errors, and it's # also necessary for go vet to work properly (see #807). - gvm use stable && godep go install $(go list ./... | grep -v "/vendor/"): pwd: $BASE_STABLE # FMT - gvm use stable && make fmt: pwd: $BASE_STABLE # VET - gvm use stable && make vet: pwd: $BASE_STABLE # LINT - gvm use stable && make lint: pwd: $BASE_STABLE override: # Test stable, and report - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/$PACKAGE/coverage.out -coverpkg=$(./coverpkg.sh $PACKAGE $ROOT_PACKAGE) $PACKAGE': timeout: 1000 pwd: $BASE_STABLE # Test stable with race - gvm use stable; export ROOT_PACKAGE=$(go list .); go list -tags "$DOCKER_BUILDTAGS" ./... | grep -v "/vendor/" | grep -v "registry/handlers" | grep -v "registry/storage/driver" | xargs -L 1 -I{} bash -c 'export PACKAGE={}; godep go test -race -tags "$DOCKER_BUILDTAGS" -test.short $PACKAGE': timeout: 1000 pwd: $BASE_STABLE post: # Report to codecov - bash <(curl -s https://codecov.io/bash): pwd: $BASE_STABLE ## Notes # Do we want these as well? # - go get code.google.com/p/go.tools/cmd/goimports # - test -z "$(goimports -l -w ./... | tee /dev/stderr)" # http://labix.org/gocheck docker-registry-2.6.2~ds1/cmd/000077500000000000000000000000001313450123100162005ustar00rootroot00000000000000docker-registry-2.6.2~ds1/cmd/digest/000077500000000000000000000000001313450123100174575ustar00rootroot00000000000000docker-registry-2.6.2~ds1/cmd/digest/main.go000066400000000000000000000033161313450123100207350ustar00rootroot00000000000000package main import ( "flag" "fmt" "io" "log" "os" "github.com/docker/distribution/digest" "github.com/docker/distribution/version" ) var ( algorithm = digest.Canonical showVersion bool ) type job struct { name string reader io.Reader } func init() { flag.Var(&algorithm, "a", "select the digest algorithm (shorthand)") flag.Var(&algorithm, "algorithm", "select the digest algorithm") flag.BoolVar(&showVersion, "version", false, "show the version and exit") log.SetFlags(0) log.SetPrefix(os.Args[0] + ": ") } func usage() { fmt.Fprintf(os.Stderr, "usage: %s [files...]\n", os.Args[0]) fmt.Fprintf(os.Stderr, ` Calculate the digest of one or more input files, emitting the result to standard out. If no files are provided, the digest of stdin will be calculated. `) flag.PrintDefaults() } func unsupported() { log.Fatalf("unsupported digest algorithm: %v", algorithm) } func main() { var jobs []job flag.Usage = usage flag.Parse() if showVersion { version.PrintVersion() return } var fail bool // if we fail on one item, foul the exit code if flag.NArg() > 0 { for _, path := range flag.Args() { fp, err := os.Open(path) if err != nil { log.Printf("%s: %v", path, err) fail = true continue } defer fp.Close() jobs = append(jobs, job{name: path, reader: fp}) } } else { // just read stdin jobs = append(jobs, job{name: "-", reader: os.Stdin}) } digestFn := algorithm.FromReader if !algorithm.Available() { unsupported() } for _, job := range jobs { dgst, err := digestFn(job.reader) if err != nil { log.Printf("%s: %v", job.name, err) fail = true continue } fmt.Printf("%v\t%s\n", dgst, job.name) } if fail { os.Exit(1) } } docker-registry-2.6.2~ds1/cmd/registry-api-descriptor-template/000077500000000000000000000000001313450123100246045ustar00rootroot00000000000000docker-registry-2.6.2~ds1/cmd/registry-api-descriptor-template/main.go000066400000000000000000000053531313450123100260650ustar00rootroot00000000000000// registry-api-descriptor-template uses the APIDescriptor defined in the // api/v2 package to execute templates passed to the command line. // // For example, to generate a new API specification, one would execute the // following command from the repo root: // // $ registry-api-descriptor-template docs/spec/api.md.tmpl > docs/spec/api.md // // The templates are passed in the api/v2.APIDescriptor object. Please see the // package documentation for fields available on that object. The template // syntax is from Go's standard library text/template package. For information // on Go's template syntax, please see golang.org/pkg/text/template. package main import ( "log" "net/http" "os" "path/filepath" "regexp" "text/template" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" ) var spaceRegex = regexp.MustCompile(`\n\s*`) func main() { if len(os.Args) != 2 { log.Fatalln("please specify a template to execute.") } path := os.Args[1] filename := filepath.Base(path) funcMap := template.FuncMap{ "removenewlines": func(s string) string { return spaceRegex.ReplaceAllString(s, " ") }, "statustext": http.StatusText, "prettygorilla": prettyGorillaMuxPath, } tmpl := template.Must(template.New(filename).Funcs(funcMap).ParseFiles(path)) data := struct { RouteDescriptors []v2.RouteDescriptor ErrorDescriptors []errcode.ErrorDescriptor }{ RouteDescriptors: v2.APIDescriptor.RouteDescriptors, ErrorDescriptors: append(errcode.GetErrorCodeGroup("registry.api.v2"), // The following are part of the specification but provided by errcode default. errcode.ErrorCodeUnauthorized.Descriptor(), errcode.ErrorCodeDenied.Descriptor(), errcode.ErrorCodeUnsupported.Descriptor()), } if err := tmpl.Execute(os.Stdout, data); err != nil { log.Fatalln(err) } } // prettyGorillaMuxPath removes the regular expressions from a gorilla/mux // route string, making it suitable for documentation. func prettyGorillaMuxPath(s string) string { // Stateful parser that removes regular expressions from gorilla // routes. It correctly handles balanced bracket pairs. var output string var label string var level int start: if s[0] == '{' { s = s[1:] level++ goto capture } output += string(s[0]) s = s[1:] goto end capture: switch s[0] { case '{': level++ case '}': level-- if level == 0 { s = s[1:] goto label } case ':': s = s[1:] goto skip default: label += string(s[0]) } s = s[1:] goto capture skip: switch s[0] { case '{': level++ case '}': level-- } s = s[1:] if level == 0 { goto label } goto skip label: if label != "" { output += "<" + label + ">" label = "" } end: if s != "" { goto start } return output } docker-registry-2.6.2~ds1/cmd/registry/000077500000000000000000000000001313450123100200505ustar00rootroot00000000000000docker-registry-2.6.2~ds1/cmd/registry/config-cache.yml000066400000000000000000000022471313450123100231060ustar00rootroot00000000000000version: 0.1 log: level: debug fields: service: registry environment: development storage: cache: blobdescriptor: redis filesystem: rootdirectory: /var/lib/registry-cache maintenance: uploadpurging: enabled: false http: addr: :5000 secret: asecretforlocaldevelopment debug: addr: localhost:5001 headers: X-Content-Type-Options: [nosniff] redis: addr: localhost:6379 pool: maxidle: 16 maxactive: 64 idletimeout: 300s dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms notifications: endpoints: - name: local-8082 url: http://localhost:5003/callback headers: Authorization: [Bearer ] timeout: 1s threshold: 10 backoff: 1s disabled: true - name: local-8083 url: http://localhost:8083/callback timeout: 1s threshold: 10 backoff: 1s disabled: true proxy: remoteurl: https://registry-1.docker.io username: username password: password health: storagedriver: enabled: true interval: 10s threshold: 3 docker-registry-2.6.2~ds1/cmd/registry/config-dev.yml000066400000000000000000000025551313450123100226230ustar00rootroot00000000000000version: 0.1 log: level: debug fields: service: registry environment: development hooks: - type: mail disabled: true levels: - panic options: smtp: addr: mail.example.com:25 username: mailuser password: password insecure: true from: sender@example.com to: - errors@example.com storage: delete: enabled: true cache: blobdescriptor: redis filesystem: rootdirectory: /var/lib/registry maintenance: uploadpurging: enabled: false http: addr: :5000 debug: addr: localhost:5001 headers: X-Content-Type-Options: [nosniff] redis: addr: localhost:6379 pool: maxidle: 16 maxactive: 64 idletimeout: 300s dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms notifications: endpoints: - name: local-5003 url: http://localhost:5003/callback headers: Authorization: [Bearer ] timeout: 1s threshold: 10 backoff: 1s disabled: true - name: local-8083 url: http://localhost:8083/callback timeout: 1s threshold: 10 backoff: 1s disabled: true health: storagedriver: enabled: true interval: 10s threshold: 3 docker-registry-2.6.2~ds1/cmd/registry/config-example.yml000066400000000000000000000004471313450123100234760ustar00rootroot00000000000000version: 0.1 log: fields: service: registry storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /var/lib/registry http: addr: :5000 headers: X-Content-Type-Options: [nosniff] health: storagedriver: enabled: true interval: 10s threshold: 3 docker-registry-2.6.2~ds1/cmd/registry/main.go000066400000000000000000000020361313450123100213240ustar00rootroot00000000000000package main import ( _ "net/http/pprof" "github.com/docker/distribution/registry" _ "github.com/docker/distribution/registry/auth/htpasswd" _ "github.com/docker/distribution/registry/auth/silly" _ "github.com/docker/distribution/registry/auth/token" _ "github.com/docker/distribution/registry/proxy" _ "github.com/docker/distribution/registry/storage/driver/azure" _ "github.com/docker/distribution/registry/storage/driver/filesystem" _ "github.com/docker/distribution/registry/storage/driver/gcs" _ "github.com/docker/distribution/registry/storage/driver/inmemory" _ "github.com/docker/distribution/registry/storage/driver/middleware/cloudfront" _ "github.com/docker/distribution/registry/storage/driver/middleware/redirect" _ "github.com/docker/distribution/registry/storage/driver/oss" _ "github.com/docker/distribution/registry/storage/driver/s3-aws" _ "github.com/docker/distribution/registry/storage/driver/s3-goamz" _ "github.com/docker/distribution/registry/storage/driver/swift" ) func main() { registry.RootCmd.Execute() } docker-registry-2.6.2~ds1/configuration/000077500000000000000000000000001313450123100203045ustar00rootroot00000000000000docker-registry-2.6.2~ds1/configuration/configuration.go000066400000000000000000000533001313450123100235030ustar00rootroot00000000000000package configuration import ( "fmt" "io" "io/ioutil" "net/http" "reflect" "strings" "time" ) // Configuration is a versioned registry configuration, intended to be provided by a yaml file, and // optionally modified by environment variables. // // Note that yaml field names should never include _ characters, since this is the separator used // in environment variable names. type Configuration struct { // Version is the version which defines the format of the rest of the configuration Version Version `yaml:"version"` // Log supports setting various parameters related to the logging // subsystem. Log struct { // AccessLog configures access logging. AccessLog struct { // Disabled disables access logging. Disabled bool `yaml:"disabled,omitempty"` } `yaml:"accesslog,omitempty"` // Level is the granularity at which registry operations are logged. Level Loglevel `yaml:"level"` // Formatter overrides the default formatter with another. Options // include "text", "json" and "logstash". Formatter string `yaml:"formatter,omitempty"` // Fields allows users to specify static string fields to include in // the logger context. Fields map[string]interface{} `yaml:"fields,omitempty"` // Hooks allows users to configure the log hooks, to enabling the // sequent handling behavior, when defined levels of log message emit. Hooks []LogHook `yaml:"hooks,omitempty"` } // Loglevel is the level at which registry operations are logged. This is // deprecated. Please use Log.Level in the future. Loglevel Loglevel `yaml:"loglevel,omitempty"` // Storage is the configuration for the registry's storage driver Storage Storage `yaml:"storage"` // Auth allows configuration of various authorization methods that may be // used to gate requests. Auth Auth `yaml:"auth,omitempty"` // Middleware lists all middlewares to be used by the registry. Middleware map[string][]Middleware `yaml:"middleware,omitempty"` // Reporting is the configuration for error reporting Reporting Reporting `yaml:"reporting,omitempty"` // HTTP contains configuration parameters for the registry's http // interface. HTTP struct { // Addr specifies the bind address for the registry instance. Addr string `yaml:"addr,omitempty"` // Net specifies the net portion of the bind address. A default empty value means tcp. Net string `yaml:"net,omitempty"` // Host specifies an externally-reachable address for the registry, as a fully // qualified URL. Host string `yaml:"host,omitempty"` Prefix string `yaml:"prefix,omitempty"` // Secret specifies the secret key which HMAC tokens are created with. Secret string `yaml:"secret,omitempty"` // RelativeURLs specifies that relative URLs should be returned in // Location headers RelativeURLs bool `yaml:"relativeurls,omitempty"` // TLS instructs the http server to listen with a TLS configuration. // This only support simple tls configuration with a cert and key. // Mostly, this is useful for testing situations or simple deployments // that require tls. If more complex configurations are required, use // a proxy or make a proposal to add support here. TLS struct { // Certificate specifies the path to an x509 certificate file to // be used for TLS. Certificate string `yaml:"certificate,omitempty"` // Key specifies the path to the x509 key file, which should // contain the private portion for the file specified in // Certificate. Key string `yaml:"key,omitempty"` // Specifies the CA certs for client authentication // A file may contain multiple CA certificates encoded as PEM ClientCAs []string `yaml:"clientcas,omitempty"` // LetsEncrypt is used to configuration setting up TLS through // Let's Encrypt instead of manually specifying certificate and // key. If a TLS certificate is specified, the Let's Encrypt // section will not be used. LetsEncrypt struct { // CacheFile specifies cache file to use for lets encrypt // certificates and keys. CacheFile string `yaml:"cachefile,omitempty"` // Email is the email to use during Let's Encrypt registration Email string `yaml:"email,omitempty"` } `yaml:"letsencrypt,omitempty"` } `yaml:"tls,omitempty"` // Headers is a set of headers to include in HTTP responses. A common // use case for this would be security headers such as // Strict-Transport-Security. The map keys are the header names, and // the values are the associated header payloads. Headers http.Header `yaml:"headers,omitempty"` // Debug configures the http debug interface, if specified. This can // include services such as pprof, expvar and other data that should // not be exposed externally. Left disabled by default. Debug struct { // Addr specifies the bind address for the debug server. Addr string `yaml:"addr,omitempty"` } `yaml:"debug,omitempty"` // HTTP2 configuration options HTTP2 struct { // Specifies wether the registry should disallow clients attempting // to connect via http2. If set to true, only http/1.1 is supported. Disabled bool `yaml:"disabled,omitempty"` } `yaml:"http2,omitempty"` } `yaml:"http,omitempty"` // Notifications specifies configuration about various endpoint to which // registry events are dispatched. Notifications Notifications `yaml:"notifications,omitempty"` // Redis configures the redis pool available to the registry webapp. Redis struct { // Addr specifies the the redis instance available to the application. Addr string `yaml:"addr,omitempty"` // Password string to use when making a connection. Password string `yaml:"password,omitempty"` // DB specifies the database to connect to on the redis instance. DB int `yaml:"db,omitempty"` DialTimeout time.Duration `yaml:"dialtimeout,omitempty"` // timeout for connect ReadTimeout time.Duration `yaml:"readtimeout,omitempty"` // timeout for reads of data WriteTimeout time.Duration `yaml:"writetimeout,omitempty"` // timeout for writes of data // Pool configures the behavior of the redis connection pool. Pool struct { // MaxIdle sets the maximum number of idle connections. MaxIdle int `yaml:"maxidle,omitempty"` // MaxActive sets the maximum number of connections that should be // opened before blocking a connection request. MaxActive int `yaml:"maxactive,omitempty"` // IdleTimeout sets the amount time to wait before closing // inactive connections. IdleTimeout time.Duration `yaml:"idletimeout,omitempty"` } `yaml:"pool,omitempty"` } `yaml:"redis,omitempty"` Health Health `yaml:"health,omitempty"` Proxy Proxy `yaml:"proxy,omitempty"` // Compatibility is used for configurations of working with older or deprecated features. Compatibility struct { // Schema1 configures how schema1 manifests will be handled Schema1 struct { // TrustKey is the signing key to use for adding the signature to // schema1 manifests. TrustKey string `yaml:"signingkeyfile,omitempty"` } `yaml:"schema1,omitempty"` } `yaml:"compatibility,omitempty"` // Validation configures validation options for the registry. Validation struct { // Enabled enables the other options in this section. Enabled bool `yaml:"enabled,omitempty"` // Manifests configures manifest validation. Manifests struct { // URLs configures validation for URLs in pushed manifests. URLs struct { // Allow specifies regular expressions (https://godoc.org/regexp/syntax) // that URLs in pushed manifests must match. Allow []string `yaml:"allow,omitempty"` // Deny specifies regular expressions (https://godoc.org/regexp/syntax) // that URLs in pushed manifests must not match. Deny []string `yaml:"deny,omitempty"` } `yaml:"urls,omitempty"` } `yaml:"manifests,omitempty"` } `yaml:"validation,omitempty"` // Policy configures registry policy options. Policy struct { // Repository configures policies for repositories Repository struct { // Classes is a list of repository classes which the // registry allows content for. This class is matched // against the configuration media type inside uploaded // manifests. When non-empty, the registry will enforce // the class in authorized resources. Classes []string `yaml:"classes"` } `yaml:"repository,omitempty"` } `yaml:"policy,omitempty"` } // LogHook is composed of hook Level and Type. // After hooks configuration, it can execute the next handling automatically, // when defined levels of log message emitted. // Example: hook can sending an email notification when error log happens in app. type LogHook struct { // Disable lets user select to enable hook or not. Disabled bool `yaml:"disabled,omitempty"` // Type allows user to select which type of hook handler they want. Type string `yaml:"type,omitempty"` // Levels set which levels of log message will let hook executed. Levels []string `yaml:"levels,omitempty"` // MailOptions allows user to configurate email parameters. MailOptions MailOptions `yaml:"options,omitempty"` } // MailOptions provides the configuration sections to user, for specific handler. type MailOptions struct { SMTP struct { // Addr defines smtp host address Addr string `yaml:"addr,omitempty"` // Username defines user name to smtp host Username string `yaml:"username,omitempty"` // Password defines password of login user Password string `yaml:"password,omitempty"` // Insecure defines if smtp login skips the secure certification. Insecure bool `yaml:"insecure,omitempty"` } `yaml:"smtp,omitempty"` // From defines mail sending address From string `yaml:"from,omitempty"` // To defines mail receiving address To []string `yaml:"to,omitempty"` } // FileChecker is a type of entry in the health section for checking files. type FileChecker struct { // Interval is the duration in between checks Interval time.Duration `yaml:"interval,omitempty"` // File is the path to check File string `yaml:"file,omitempty"` // Threshold is the number of times a check must fail to trigger an // unhealthy state Threshold int `yaml:"threshold,omitempty"` } // HTTPChecker is a type of entry in the health section for checking HTTP URIs. type HTTPChecker struct { // Timeout is the duration to wait before timing out the HTTP request Timeout time.Duration `yaml:"timeout,omitempty"` // StatusCode is the expected status code StatusCode int // Interval is the duration in between checks Interval time.Duration `yaml:"interval,omitempty"` // URI is the HTTP URI to check URI string `yaml:"uri,omitempty"` // Headers lists static headers that should be added to all requests Headers http.Header `yaml:"headers"` // Threshold is the number of times a check must fail to trigger an // unhealthy state Threshold int `yaml:"threshold,omitempty"` } // TCPChecker is a type of entry in the health section for checking TCP servers. type TCPChecker struct { // Timeout is the duration to wait before timing out the TCP connection Timeout time.Duration `yaml:"timeout,omitempty"` // Interval is the duration in between checks Interval time.Duration `yaml:"interval,omitempty"` // Addr is the TCP address to check Addr string `yaml:"addr,omitempty"` // Threshold is the number of times a check must fail to trigger an // unhealthy state Threshold int `yaml:"threshold,omitempty"` } // Health provides the configuration section for health checks. type Health struct { // FileCheckers is a list of paths to check FileCheckers []FileChecker `yaml:"file,omitempty"` // HTTPCheckers is a list of URIs to check HTTPCheckers []HTTPChecker `yaml:"http,omitempty"` // TCPCheckers is a list of URIs to check TCPCheckers []TCPChecker `yaml:"tcp,omitempty"` // StorageDriver configures a health check on the configured storage // driver StorageDriver struct { // Enabled turns on the health check for the storage driver Enabled bool `yaml:"enabled,omitempty"` // Interval is the duration in between checks Interval time.Duration `yaml:"interval,omitempty"` // Threshold is the number of times a check must fail to trigger an // unhealthy state Threshold int `yaml:"threshold,omitempty"` } `yaml:"storagedriver,omitempty"` } // v0_1Configuration is a Version 0.1 Configuration struct // This is currently aliased to Configuration, as it is the current version type v0_1Configuration Configuration // UnmarshalYAML implements the yaml.Unmarshaler interface // Unmarshals a string of the form X.Y into a Version, validating that X and Y can represent uints func (version *Version) UnmarshalYAML(unmarshal func(interface{}) error) error { var versionString string err := unmarshal(&versionString) if err != nil { return err } newVersion := Version(versionString) if _, err := newVersion.major(); err != nil { return err } if _, err := newVersion.minor(); err != nil { return err } *version = newVersion return nil } // CurrentVersion is the most recent Version that can be parsed var CurrentVersion = MajorMinorVersion(0, 1) // Loglevel is the level at which operations are logged // This can be error, warn, info, or debug type Loglevel string // UnmarshalYAML implements the yaml.Umarshaler interface // Unmarshals a string into a Loglevel, lowercasing the string and validating that it represents a // valid loglevel func (loglevel *Loglevel) UnmarshalYAML(unmarshal func(interface{}) error) error { var loglevelString string err := unmarshal(&loglevelString) if err != nil { return err } loglevelString = strings.ToLower(loglevelString) switch loglevelString { case "error", "warn", "info", "debug": default: return fmt.Errorf("Invalid loglevel %s Must be one of [error, warn, info, debug]", loglevelString) } *loglevel = Loglevel(loglevelString) return nil } // Parameters defines a key-value parameters mapping type Parameters map[string]interface{} // Storage defines the configuration for registry object storage type Storage map[string]Parameters // Type returns the storage driver type, such as filesystem or s3 func (storage Storage) Type() string { var storageType []string // Return only key in this map for k := range storage { switch k { case "maintenance": // allow configuration of maintenance case "cache": // allow configuration of caching case "delete": // allow configuration of delete case "redirect": // allow configuration of redirect default: storageType = append(storageType, k) } } if len(storageType) > 1 { panic("multiple storage drivers specified in configuration or environment: " + strings.Join(storageType, ", ")) } if len(storageType) == 1 { return storageType[0] } return "" } // Parameters returns the Parameters map for a Storage configuration func (storage Storage) Parameters() Parameters { return storage[storage.Type()] } // setParameter changes the parameter at the provided key to the new value func (storage Storage) setParameter(key string, value interface{}) { storage[storage.Type()][key] = value } // UnmarshalYAML implements the yaml.Unmarshaler interface // Unmarshals a single item map into a Storage or a string into a Storage type with no parameters func (storage *Storage) UnmarshalYAML(unmarshal func(interface{}) error) error { var storageMap map[string]Parameters err := unmarshal(&storageMap) if err == nil { if len(storageMap) > 1 { types := make([]string, 0, len(storageMap)) for k := range storageMap { switch k { case "maintenance": // allow for configuration of maintenance case "cache": // allow configuration of caching case "delete": // allow configuration of delete case "redirect": // allow configuration of redirect default: types = append(types, k) } } if len(types) > 1 { return fmt.Errorf("Must provide exactly one storage type. Provided: %v", types) } } *storage = storageMap return nil } var storageType string err = unmarshal(&storageType) if err == nil { *storage = Storage{storageType: Parameters{}} return nil } return err } // MarshalYAML implements the yaml.Marshaler interface func (storage Storage) MarshalYAML() (interface{}, error) { if storage.Parameters() == nil { return storage.Type(), nil } return map[string]Parameters(storage), nil } // Auth defines the configuration for registry authorization. type Auth map[string]Parameters // Type returns the auth type, such as htpasswd or token func (auth Auth) Type() string { // Return only key in this map for k := range auth { return k } return "" } // Parameters returns the Parameters map for an Auth configuration func (auth Auth) Parameters() Parameters { return auth[auth.Type()] } // setParameter changes the parameter at the provided key to the new value func (auth Auth) setParameter(key string, value interface{}) { auth[auth.Type()][key] = value } // UnmarshalYAML implements the yaml.Unmarshaler interface // Unmarshals a single item map into a Storage or a string into a Storage type with no parameters func (auth *Auth) UnmarshalYAML(unmarshal func(interface{}) error) error { var m map[string]Parameters err := unmarshal(&m) if err == nil { if len(m) > 1 { types := make([]string, 0, len(m)) for k := range m { types = append(types, k) } // TODO(stevvooe): May want to change this slightly for // authorization to allow multiple challenges. return fmt.Errorf("must provide exactly one type. Provided: %v", types) } *auth = m return nil } var authType string err = unmarshal(&authType) if err == nil { *auth = Auth{authType: Parameters{}} return nil } return err } // MarshalYAML implements the yaml.Marshaler interface func (auth Auth) MarshalYAML() (interface{}, error) { if auth.Parameters() == nil { return auth.Type(), nil } return map[string]Parameters(auth), nil } // Notifications configures multiple http endpoints. type Notifications struct { // Endpoints is a list of http configurations for endpoints that // respond to webhook notifications. In the future, we may allow other // kinds of endpoints, such as external queues. Endpoints []Endpoint `yaml:"endpoints,omitempty"` } // Endpoint describes the configuration of an http webhook notification // endpoint. type Endpoint struct { Name string `yaml:"name"` // identifies the endpoint in the registry instance. Disabled bool `yaml:"disabled"` // disables the endpoint URL string `yaml:"url"` // post url for the endpoint. Headers http.Header `yaml:"headers"` // static headers that should be added to all requests Timeout time.Duration `yaml:"timeout"` // HTTP timeout Threshold int `yaml:"threshold"` // circuit breaker threshold before backing off on failure Backoff time.Duration `yaml:"backoff"` // backoff duration IgnoredMediaTypes []string `yaml:"ignoredmediatypes"` // target media types to ignore } // Reporting defines error reporting methods. type Reporting struct { // Bugsnag configures error reporting for Bugsnag (bugsnag.com). Bugsnag BugsnagReporting `yaml:"bugsnag,omitempty"` // NewRelic configures error reporting for NewRelic (newrelic.com) NewRelic NewRelicReporting `yaml:"newrelic,omitempty"` } // BugsnagReporting configures error reporting for Bugsnag (bugsnag.com). type BugsnagReporting struct { // APIKey is the Bugsnag api key. APIKey string `yaml:"apikey,omitempty"` // ReleaseStage tracks where the registry is deployed. // Examples: production, staging, development ReleaseStage string `yaml:"releasestage,omitempty"` // Endpoint is used for specifying an enterprise Bugsnag endpoint. Endpoint string `yaml:"endpoint,omitempty"` } // NewRelicReporting configures error reporting for NewRelic (newrelic.com) type NewRelicReporting struct { // LicenseKey is the NewRelic user license key LicenseKey string `yaml:"licensekey,omitempty"` // Name is the component name of the registry in NewRelic Name string `yaml:"name,omitempty"` // Verbose configures debug output to STDOUT Verbose bool `yaml:"verbose,omitempty"` } // Middleware configures named middlewares to be applied at injection points. type Middleware struct { // Name the middleware registers itself as Name string `yaml:"name"` // Flag to disable middleware easily Disabled bool `yaml:"disabled,omitempty"` // Map of parameters that will be passed to the middleware's initialization function Options Parameters `yaml:"options"` } // Proxy configures the registry as a pull through cache type Proxy struct { // RemoteURL is the URL of the remote registry RemoteURL string `yaml:"remoteurl"` // Username of the hub user Username string `yaml:"username"` // Password of the hub user Password string `yaml:"password"` } // Parse parses an input configuration yaml document into a Configuration struct // This should generally be capable of handling old configuration format versions // // Environment variables may be used to override configuration parameters other than version, // following the scheme below: // Configuration.Abc may be replaced by the value of REGISTRY_ABC, // Configuration.Abc.Xyz may be replaced by the value of REGISTRY_ABC_XYZ, and so forth func Parse(rd io.Reader) (*Configuration, error) { in, err := ioutil.ReadAll(rd) if err != nil { return nil, err } p := NewParser("registry", []VersionedParseInfo{ { Version: MajorMinorVersion(0, 1), ParseAs: reflect.TypeOf(v0_1Configuration{}), ConversionFunc: func(c interface{}) (interface{}, error) { if v0_1, ok := c.(*v0_1Configuration); ok { if v0_1.Loglevel == Loglevel("") { v0_1.Loglevel = Loglevel("info") } if v0_1.Storage.Type() == "" { return nil, fmt.Errorf("No storage configuration provided") } return (*Configuration)(v0_1), nil } return nil, fmt.Errorf("Expected *v0_1Configuration, received %#v", c) }, }, }) config := new(Configuration) err = p.Parse(in, config) if err != nil { return nil, err } return config, nil } docker-registry-2.6.2~ds1/configuration/configuration_test.go000066400000000000000000000426621313450123100245530ustar00rootroot00000000000000package configuration import ( "bytes" "net/http" "os" "reflect" "strings" "testing" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" ) // Hook up gocheck into the "go test" runner func Test(t *testing.T) { TestingT(t) } // configStruct is a canonical example configuration, which should map to configYamlV0_1 var configStruct = Configuration{ Version: "0.1", Log: struct { AccessLog struct { Disabled bool `yaml:"disabled,omitempty"` } `yaml:"accesslog,omitempty"` Level Loglevel `yaml:"level"` Formatter string `yaml:"formatter,omitempty"` Fields map[string]interface{} `yaml:"fields,omitempty"` Hooks []LogHook `yaml:"hooks,omitempty"` }{ Fields: map[string]interface{}{"environment": "test"}, }, Loglevel: "info", Storage: Storage{ "s3": Parameters{ "region": "us-east-1", "bucket": "my-bucket", "rootdirectory": "/registry", "encrypt": true, "secure": false, "accesskey": "SAMPLEACCESSKEY", "secretkey": "SUPERSECRET", "host": nil, "port": 42, }, }, Auth: Auth{ "silly": Parameters{ "realm": "silly", "service": "silly", }, }, Reporting: Reporting{ Bugsnag: BugsnagReporting{ APIKey: "BugsnagApiKey", }, }, Notifications: Notifications{ Endpoints: []Endpoint{ { Name: "endpoint-1", URL: "http://example.com", Headers: http.Header{ "Authorization": []string{"Bearer "}, }, IgnoredMediaTypes: []string{"application/octet-stream"}, }, }, }, HTTP: struct { Addr string `yaml:"addr,omitempty"` Net string `yaml:"net,omitempty"` Host string `yaml:"host,omitempty"` Prefix string `yaml:"prefix,omitempty"` Secret string `yaml:"secret,omitempty"` RelativeURLs bool `yaml:"relativeurls,omitempty"` TLS struct { Certificate string `yaml:"certificate,omitempty"` Key string `yaml:"key,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"` LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` } `yaml:"letsencrypt,omitempty"` } `yaml:"tls,omitempty"` Headers http.Header `yaml:"headers,omitempty"` Debug struct { Addr string `yaml:"addr,omitempty"` } `yaml:"debug,omitempty"` HTTP2 struct { Disabled bool `yaml:"disabled,omitempty"` } `yaml:"http2,omitempty"` }{ TLS: struct { Certificate string `yaml:"certificate,omitempty"` Key string `yaml:"key,omitempty"` ClientCAs []string `yaml:"clientcas,omitempty"` LetsEncrypt struct { CacheFile string `yaml:"cachefile,omitempty"` Email string `yaml:"email,omitempty"` } `yaml:"letsencrypt,omitempty"` }{ ClientCAs: []string{"/path/to/ca.pem"}, }, Headers: http.Header{ "X-Content-Type-Options": []string{"nosniff"}, }, HTTP2: struct { Disabled bool `yaml:"disabled,omitempty"` }{ Disabled: false, }, }, } // configYamlV0_1 is a Version 0.1 yaml document representing configStruct var configYamlV0_1 = ` version: 0.1 log: fields: environment: test loglevel: info storage: s3: region: us-east-1 bucket: my-bucket rootdirectory: /registry encrypt: true secure: false accesskey: SAMPLEACCESSKEY secretkey: SUPERSECRET host: ~ port: 42 auth: silly: realm: silly service: silly notifications: endpoints: - name: endpoint-1 url: http://example.com headers: Authorization: [Bearer ] ignoredmediatypes: - application/octet-stream reporting: bugsnag: apikey: BugsnagApiKey http: clientcas: - /path/to/ca.pem headers: X-Content-Type-Options: [nosniff] ` // inmemoryConfigYamlV0_1 is a Version 0.1 yaml document specifying an inmemory // storage driver with no parameters var inmemoryConfigYamlV0_1 = ` version: 0.1 loglevel: info storage: inmemory auth: silly: realm: silly service: silly notifications: endpoints: - name: endpoint-1 url: http://example.com headers: Authorization: [Bearer ] ignoredmediatypes: - application/octet-stream http: headers: X-Content-Type-Options: [nosniff] ` type ConfigSuite struct { expectedConfig *Configuration } var _ = Suite(new(ConfigSuite)) func (suite *ConfigSuite) SetUpTest(c *C) { os.Clearenv() suite.expectedConfig = copyConfig(configStruct) } // TestMarshalRoundtrip validates that configStruct can be marshaled and // unmarshaled without changing any parameters func (suite *ConfigSuite) TestMarshalRoundtrip(c *C) { configBytes, err := yaml.Marshal(suite.expectedConfig) c.Assert(err, IsNil) config, err := Parse(bytes.NewReader(configBytes)) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseSimple validates that configYamlV0_1 can be parsed into a struct // matching configStruct func (suite *ConfigSuite) TestParseSimple(c *C) { config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseInmemory validates that configuration yaml with storage provided as // a string can be parsed into a Configuration struct with no storage parameters func (suite *ConfigSuite) TestParseInmemory(c *C) { suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Log.Fields = nil config, err := Parse(bytes.NewReader([]byte(inmemoryConfigYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseIncomplete validates that an incomplete yaml configuration cannot // be parsed without providing environment variables to fill in the missing // components. func (suite *ConfigSuite) TestParseIncomplete(c *C) { incompleteConfigYaml := "version: 0.1" _, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml))) c.Assert(err, NotNil) suite.expectedConfig.Log.Fields = nil suite.expectedConfig.Storage = Storage{"filesystem": Parameters{"rootdirectory": "/tmp/testroot"}} suite.expectedConfig.Auth = Auth{"silly": Parameters{"realm": "silly"}} suite.expectedConfig.Reporting = Reporting{} suite.expectedConfig.Notifications = Notifications{} suite.expectedConfig.HTTP.Headers = nil // Note: this also tests that REGISTRY_STORAGE and // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY can be used together os.Setenv("REGISTRY_STORAGE", "filesystem") os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot") os.Setenv("REGISTRY_AUTH", "silly") os.Setenv("REGISTRY_AUTH_SILLY_REALM", "silly") config, err := Parse(bytes.NewReader([]byte(incompleteConfigYaml))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithSameEnvStorage validates that providing environment variables // that match the given storage type will only include environment-defined // parameters and remove yaml-defined parameters func (suite *ConfigSuite) TestParseWithSameEnvStorage(c *C) { suite.expectedConfig.Storage = Storage{"s3": Parameters{"region": "us-east-1"}} os.Setenv("REGISTRY_STORAGE", "s3") os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-east-1") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithDifferentEnvStorageParams validates that providing environment variables that change // and add to the given storage parameters will change and add parameters to the parsed // Configuration struct func (suite *ConfigSuite) TestParseWithDifferentEnvStorageParams(c *C) { suite.expectedConfig.Storage.setParameter("region", "us-west-1") suite.expectedConfig.Storage.setParameter("secure", true) suite.expectedConfig.Storage.setParameter("newparam", "some Value") os.Setenv("REGISTRY_STORAGE_S3_REGION", "us-west-1") os.Setenv("REGISTRY_STORAGE_S3_SECURE", "true") os.Setenv("REGISTRY_STORAGE_S3_NEWPARAM", "some Value") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithDifferentEnvStorageType validates that providing an environment variable that // changes the storage type will be reflected in the parsed Configuration struct func (suite *ConfigSuite) TestParseWithDifferentEnvStorageType(c *C) { suite.expectedConfig.Storage = Storage{"inmemory": Parameters{}} os.Setenv("REGISTRY_STORAGE", "inmemory") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithDifferentEnvStorageTypeAndParams validates that providing an environment variable // that changes the storage type will be reflected in the parsed Configuration struct and that // environment storage parameters will also be included func (suite *ConfigSuite) TestParseWithDifferentEnvStorageTypeAndParams(c *C) { suite.expectedConfig.Storage = Storage{"filesystem": Parameters{}} suite.expectedConfig.Storage.setParameter("rootdirectory", "/tmp/testroot") os.Setenv("REGISTRY_STORAGE", "filesystem") os.Setenv("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp/testroot") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithSameEnvLoglevel validates that providing an environment variable defining the log // level to the same as the one provided in the yaml will not change the parsed Configuration struct func (suite *ConfigSuite) TestParseWithSameEnvLoglevel(c *C) { os.Setenv("REGISTRY_LOGLEVEL", "info") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseWithDifferentEnvLoglevel validates that providing an environment variable defining the // log level will override the value provided in the yaml document func (suite *ConfigSuite) TestParseWithDifferentEnvLoglevel(c *C) { suite.expectedConfig.Loglevel = "error" os.Setenv("REGISTRY_LOGLEVEL", "error") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseInvalidLoglevel validates that the parser will fail to parse a // configuration if the loglevel is malformed func (suite *ConfigSuite) TestParseInvalidLoglevel(c *C) { invalidConfigYaml := "version: 0.1\nloglevel: derp\nstorage: inmemory" _, err := Parse(bytes.NewReader([]byte(invalidConfigYaml))) c.Assert(err, NotNil) os.Setenv("REGISTRY_LOGLEVEL", "derp") _, err = Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, NotNil) } // TestParseWithDifferentEnvReporting validates that environment variables // properly override reporting parameters func (suite *ConfigSuite) TestParseWithDifferentEnvReporting(c *C) { suite.expectedConfig.Reporting.Bugsnag.APIKey = "anotherBugsnagApiKey" suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080" suite.expectedConfig.Reporting.NewRelic.LicenseKey = "NewRelicLicenseKey" suite.expectedConfig.Reporting.NewRelic.Name = "some NewRelic NAME" os.Setenv("REGISTRY_REPORTING_BUGSNAG_APIKEY", "anotherBugsnagApiKey") os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080") os.Setenv("REGISTRY_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey") os.Setenv("REGISTRY_REPORTING_NEWRELIC_NAME", "some NewRelic NAME") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseInvalidVersion validates that the parser will fail to parse a newer configuration // version than the CurrentVersion func (suite *ConfigSuite) TestParseInvalidVersion(c *C) { suite.expectedConfig.Version = MajorMinorVersion(CurrentVersion.Major(), CurrentVersion.Minor()+1) configBytes, err := yaml.Marshal(suite.expectedConfig) c.Assert(err, IsNil) _, err = Parse(bytes.NewReader(configBytes)) c.Assert(err, NotNil) } // TestParseExtraneousVars validates that environment variables referring to // nonexistent variables don't cause side effects. func (suite *ConfigSuite) TestParseExtraneousVars(c *C) { suite.expectedConfig.Reporting.Bugsnag.Endpoint = "localhost:8080" // A valid environment variable os.Setenv("REGISTRY_REPORTING_BUGSNAG_ENDPOINT", "localhost:8080") // Environment variables which shouldn't set config items os.Setenv("registry_REPORTING_NEWRELIC_LICENSEKEY", "NewRelicLicenseKey") os.Setenv("REPORTING_NEWRELIC_NAME", "some NewRelic NAME") os.Setenv("REGISTRY_DUCKS", "quack") os.Setenv("REGISTRY_REPORTING_ASDF", "ghjk") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseEnvVarImplicitMaps validates that environment variables can set // values in maps that don't already exist. func (suite *ConfigSuite) TestParseEnvVarImplicitMaps(c *C) { readonly := make(map[string]interface{}) readonly["enabled"] = true maintenance := make(map[string]interface{}) maintenance["readonly"] = readonly suite.expectedConfig.Storage["maintenance"] = maintenance os.Setenv("REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLED", "true") config, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) c.Assert(config, DeepEquals, suite.expectedConfig) } // TestParseEnvWrongTypeMap validates that incorrectly attempting to unmarshal a // string over existing map fails. func (suite *ConfigSuite) TestParseEnvWrongTypeMap(c *C) { os.Setenv("REGISTRY_STORAGE_S3", "somestring") _, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, NotNil) } // TestParseEnvWrongTypeStruct validates that incorrectly attempting to // unmarshal a string into a struct fails. func (suite *ConfigSuite) TestParseEnvWrongTypeStruct(c *C) { os.Setenv("REGISTRY_STORAGE_LOG", "somestring") _, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, NotNil) } // TestParseEnvWrongTypeSlice validates that incorrectly attempting to // unmarshal a string into a slice fails. func (suite *ConfigSuite) TestParseEnvWrongTypeSlice(c *C) { os.Setenv("REGISTRY_LOG_HOOKS", "somestring") _, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, NotNil) } // TestParseEnvMany tests several environment variable overrides. // The result is not checked - the goal of this test is to detect panics // from misuse of reflection. func (suite *ConfigSuite) TestParseEnvMany(c *C) { os.Setenv("REGISTRY_VERSION", "0.1") os.Setenv("REGISTRY_LOG_LEVEL", "debug") os.Setenv("REGISTRY_LOG_FORMATTER", "json") os.Setenv("REGISTRY_LOG_HOOKS", "json") os.Setenv("REGISTRY_LOG_FIELDS", "abc: xyz") os.Setenv("REGISTRY_LOG_HOOKS", "- type: asdf") os.Setenv("REGISTRY_LOGLEVEL", "debug") os.Setenv("REGISTRY_STORAGE", "s3") os.Setenv("REGISTRY_AUTH_PARAMS", "param1: value1") os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2") os.Setenv("REGISTRY_AUTH_PARAMS_VALUE2", "value2") _, err := Parse(bytes.NewReader([]byte(configYamlV0_1))) c.Assert(err, IsNil) } func checkStructs(c *C, t reflect.Type, structsChecked map[string]struct{}) { for t.Kind() == reflect.Ptr || t.Kind() == reflect.Map || t.Kind() == reflect.Slice { t = t.Elem() } if t.Kind() != reflect.Struct { return } if _, present := structsChecked[t.String()]; present { // Already checked this type return } structsChecked[t.String()] = struct{}{} byUpperCase := make(map[string]int) for i := 0; i < t.NumField(); i++ { sf := t.Field(i) // Check that the yaml tag does not contain an _. yamlTag := sf.Tag.Get("yaml") if strings.Contains(yamlTag, "_") { c.Fatalf("yaml field name includes _ character: %s", yamlTag) } upper := strings.ToUpper(sf.Name) if _, present := byUpperCase[upper]; present { c.Fatalf("field name collision in configuration object: %s", sf.Name) } byUpperCase[upper] = i checkStructs(c, sf.Type, structsChecked) } } // TestValidateConfigStruct makes sure that the config struct has no members // with yaml tags that would be ambiguous to the environment variable parser. func (suite *ConfigSuite) TestValidateConfigStruct(c *C) { structsChecked := make(map[string]struct{}) checkStructs(c, reflect.TypeOf(Configuration{}), structsChecked) } func copyConfig(config Configuration) *Configuration { configCopy := new(Configuration) configCopy.Version = MajorMinorVersion(config.Version.Major(), config.Version.Minor()) configCopy.Loglevel = config.Loglevel configCopy.Log = config.Log configCopy.Log.Fields = make(map[string]interface{}, len(config.Log.Fields)) for k, v := range config.Log.Fields { configCopy.Log.Fields[k] = v } configCopy.Storage = Storage{config.Storage.Type(): Parameters{}} for k, v := range config.Storage.Parameters() { configCopy.Storage.setParameter(k, v) } configCopy.Reporting = Reporting{ Bugsnag: BugsnagReporting{config.Reporting.Bugsnag.APIKey, config.Reporting.Bugsnag.ReleaseStage, config.Reporting.Bugsnag.Endpoint}, NewRelic: NewRelicReporting{config.Reporting.NewRelic.LicenseKey, config.Reporting.NewRelic.Name, config.Reporting.NewRelic.Verbose}, } configCopy.Auth = Auth{config.Auth.Type(): Parameters{}} for k, v := range config.Auth.Parameters() { configCopy.Auth.setParameter(k, v) } configCopy.Notifications = Notifications{Endpoints: []Endpoint{}} for _, v := range config.Notifications.Endpoints { configCopy.Notifications.Endpoints = append(configCopy.Notifications.Endpoints, v) } configCopy.HTTP.Headers = make(http.Header) for k, v := range config.HTTP.Headers { configCopy.HTTP.Headers[k] = v } return configCopy } docker-registry-2.6.2~ds1/configuration/parser.go000066400000000000000000000177031313450123100221370ustar00rootroot00000000000000package configuration import ( "fmt" "os" "reflect" "sort" "strconv" "strings" "github.com/Sirupsen/logrus" "gopkg.in/yaml.v2" ) // Version is a major/minor version pair of the form Major.Minor // Major version upgrades indicate structure or type changes // Minor version upgrades should be strictly additive type Version string // MajorMinorVersion constructs a Version from its Major and Minor components func MajorMinorVersion(major, minor uint) Version { return Version(fmt.Sprintf("%d.%d", major, minor)) } func (version Version) major() (uint, error) { majorPart := strings.Split(string(version), ".")[0] major, err := strconv.ParseUint(majorPart, 10, 0) return uint(major), err } // Major returns the major version portion of a Version func (version Version) Major() uint { major, _ := version.major() return major } func (version Version) minor() (uint, error) { minorPart := strings.Split(string(version), ".")[1] minor, err := strconv.ParseUint(minorPart, 10, 0) return uint(minor), err } // Minor returns the minor version portion of a Version func (version Version) Minor() uint { minor, _ := version.minor() return minor } // VersionedParseInfo defines how a specific version of a configuration should // be parsed into the current version type VersionedParseInfo struct { // Version is the version which this parsing information relates to Version Version // ParseAs defines the type which a configuration file of this version // should be parsed into ParseAs reflect.Type // ConversionFunc defines a method for converting the parsed configuration // (of type ParseAs) into the current configuration version // Note: this method signature is very unclear with the absence of generics ConversionFunc func(interface{}) (interface{}, error) } type envVar struct { name string value string } type envVars []envVar func (a envVars) Len() int { return len(a) } func (a envVars) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a envVars) Less(i, j int) bool { return a[i].name < a[j].name } // Parser can be used to parse a configuration file and environment of a defined // version into a unified output structure type Parser struct { prefix string mapping map[Version]VersionedParseInfo env envVars } // NewParser returns a *Parser with the given environment prefix which handles // versioned configurations which match the given parseInfos func NewParser(prefix string, parseInfos []VersionedParseInfo) *Parser { p := Parser{prefix: prefix, mapping: make(map[Version]VersionedParseInfo)} for _, parseInfo := range parseInfos { p.mapping[parseInfo.Version] = parseInfo } for _, env := range os.Environ() { envParts := strings.SplitN(env, "=", 2) p.env = append(p.env, envVar{envParts[0], envParts[1]}) } // We must sort the environment variables lexically by name so that // more specific variables are applied before less specific ones // (i.e. REGISTRY_STORAGE before // REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY). This sucks, but it's a // lot simpler and easier to get right than unmarshalling map entries // into temporaries and merging with the existing entry. sort.Sort(p.env) return &p } // Parse reads in the given []byte and environment and writes the resulting // configuration into the input v // // Environment variables may be used to override configuration parameters other // than version, following the scheme below: // v.Abc may be replaced by the value of PREFIX_ABC, // v.Abc.Xyz may be replaced by the value of PREFIX_ABC_XYZ, and so forth func (p *Parser) Parse(in []byte, v interface{}) error { var versionedStruct struct { Version Version } if err := yaml.Unmarshal(in, &versionedStruct); err != nil { return err } parseInfo, ok := p.mapping[versionedStruct.Version] if !ok { return fmt.Errorf("Unsupported version: %q", versionedStruct.Version) } parseAs := reflect.New(parseInfo.ParseAs) err := yaml.Unmarshal(in, parseAs.Interface()) if err != nil { return err } for _, envVar := range p.env { pathStr := envVar.name if strings.HasPrefix(pathStr, strings.ToUpper(p.prefix)+"_") { path := strings.Split(pathStr, "_") err = p.overwriteFields(parseAs, pathStr, path[1:], envVar.value) if err != nil { return err } } } c, err := parseInfo.ConversionFunc(parseAs.Interface()) if err != nil { return err } reflect.ValueOf(v).Elem().Set(reflect.Indirect(reflect.ValueOf(c))) return nil } // overwriteFields replaces configuration values with alternate values specified // through the environment. Precondition: an empty path slice must never be // passed in. func (p *Parser) overwriteFields(v reflect.Value, fullpath string, path []string, payload string) error { for v.Kind() == reflect.Ptr { if v.IsNil() { panic("encountered nil pointer while handling environment variable " + fullpath) } v = reflect.Indirect(v) } switch v.Kind() { case reflect.Struct: return p.overwriteStruct(v, fullpath, path, payload) case reflect.Map: return p.overwriteMap(v, fullpath, path, payload) case reflect.Interface: if v.NumMethod() == 0 { if !v.IsNil() { return p.overwriteFields(v.Elem(), fullpath, path, payload) } // Interface was empty; create an implicit map var template map[string]interface{} wrappedV := reflect.MakeMap(reflect.TypeOf(template)) v.Set(wrappedV) return p.overwriteMap(wrappedV, fullpath, path, payload) } } return nil } func (p *Parser) overwriteStruct(v reflect.Value, fullpath string, path []string, payload string) error { // Generate case-insensitive map of struct fields byUpperCase := make(map[string]int) for i := 0; i < v.NumField(); i++ { sf := v.Type().Field(i) upper := strings.ToUpper(sf.Name) if _, present := byUpperCase[upper]; present { panic(fmt.Sprintf("field name collision in configuration object: %s", sf.Name)) } byUpperCase[upper] = i } fieldIndex, present := byUpperCase[path[0]] if !present { logrus.Warnf("Ignoring unrecognized environment variable %s", fullpath) return nil } field := v.Field(fieldIndex) sf := v.Type().Field(fieldIndex) if len(path) == 1 { // Env var specifies this field directly fieldVal := reflect.New(sf.Type) err := yaml.Unmarshal([]byte(payload), fieldVal.Interface()) if err != nil { return err } field.Set(reflect.Indirect(fieldVal)) return nil } // If the field is nil, must create an object switch sf.Type.Kind() { case reflect.Map: if field.IsNil() { field.Set(reflect.MakeMap(sf.Type)) } case reflect.Ptr: if field.IsNil() { field.Set(reflect.New(sf.Type)) } } err := p.overwriteFields(field, fullpath, path[1:], payload) if err != nil { return err } return nil } func (p *Parser) overwriteMap(m reflect.Value, fullpath string, path []string, payload string) error { if m.Type().Key().Kind() != reflect.String { // non-string keys unsupported logrus.Warnf("Ignoring environment variable %s involving map with non-string keys", fullpath) return nil } if len(path) > 1 { // If a matching key exists, get its value and continue the // overwriting process. for _, k := range m.MapKeys() { if strings.ToUpper(k.String()) == path[0] { mapValue := m.MapIndex(k) // If the existing value is nil, we want to // recreate it instead of using this value. if (mapValue.Kind() == reflect.Ptr || mapValue.Kind() == reflect.Interface || mapValue.Kind() == reflect.Map) && mapValue.IsNil() { break } return p.overwriteFields(mapValue, fullpath, path[1:], payload) } } } // (Re)create this key var mapValue reflect.Value if m.Type().Elem().Kind() == reflect.Map { mapValue = reflect.MakeMap(m.Type().Elem()) } else { mapValue = reflect.New(m.Type().Elem()) } if len(path) > 1 { err := p.overwriteFields(mapValue, fullpath, path[1:], payload) if err != nil { return err } } else { err := yaml.Unmarshal([]byte(payload), mapValue.Interface()) if err != nil { return err } } m.SetMapIndex(reflect.ValueOf(strings.ToLower(path[0])), reflect.Indirect(mapValue)) return nil } docker-registry-2.6.2~ds1/context/000077500000000000000000000000001313450123100171215ustar00rootroot00000000000000docker-registry-2.6.2~ds1/context/context.go000066400000000000000000000043521313450123100211400ustar00rootroot00000000000000package context import ( "sync" "github.com/docker/distribution/uuid" "golang.org/x/net/context" ) // Context is a copy of Context from the golang.org/x/net/context package. type Context interface { context.Context } // instanceContext is a context that provides only an instance id. It is // provided as the main background context. type instanceContext struct { Context id string // id of context, logged as "instance.id" once sync.Once // once protect generation of the id } func (ic *instanceContext) Value(key interface{}) interface{} { if key == "instance.id" { ic.once.Do(func() { // We want to lazy initialize the UUID such that we don't // call a random generator from the package initialization // code. For various reasons random could not be available // https://github.com/docker/distribution/issues/782 ic.id = uuid.Generate().String() }) return ic.id } return ic.Context.Value(key) } var background = &instanceContext{ Context: context.Background(), } // Background returns a non-nil, empty Context. The background context // provides a single key, "instance.id" that is globally unique to the // process. func Background() Context { return background } // WithValue returns a copy of parent in which the value associated with key is // val. Use context Values only for request-scoped data that transits processes // and APIs, not for passing optional parameters to functions. func WithValue(parent Context, key, val interface{}) Context { return context.WithValue(parent, key, val) } // stringMapContext is a simple context implementation that checks a map for a // key, falling back to a parent if not present. type stringMapContext struct { context.Context m map[string]interface{} } // WithValues returns a context that proxies lookups through a map. Only // supports string keys. func WithValues(ctx context.Context, m map[string]interface{}) context.Context { mo := make(map[string]interface{}, len(m)) // make our own copy. for k, v := range m { mo[k] = v } return stringMapContext{ Context: ctx, m: mo, } } func (smc stringMapContext) Value(key interface{}) interface{} { if ks, ok := key.(string); ok { if v, ok := smc.m[ks]; ok { return v } } return smc.Context.Value(key) } docker-registry-2.6.2~ds1/context/doc.go000066400000000000000000000075611313450123100202260ustar00rootroot00000000000000// Package context provides several utilities for working with // golang.org/x/net/context in http requests. Primarily, the focus is on // logging relevant request information but this package is not limited to // that purpose. // // The easiest way to get started is to get the background context: // // ctx := context.Background() // // The returned context should be passed around your application and be the // root of all other context instances. If the application has a version, this // line should be called before anything else: // // ctx := context.WithVersion(context.Background(), version) // // The above will store the version in the context and will be available to // the logger. // // Logging // // The most useful aspect of this package is GetLogger. This function takes // any context.Context interface and returns the current logger from the // context. Canonical usage looks like this: // // GetLogger(ctx).Infof("something interesting happened") // // GetLogger also takes optional key arguments. The keys will be looked up in // the context and reported with the logger. The following example would // return a logger that prints the version with each log message: // // ctx := context.Context(context.Background(), "version", version) // GetLogger(ctx, "version").Infof("this log message has a version field") // // The above would print out a log message like this: // // INFO[0000] this log message has a version field version=v2.0.0-alpha.2.m // // When used with WithLogger, we gain the ability to decorate the context with // loggers that have information from disparate parts of the call stack. // Following from the version example, we can build a new context with the // configured logger such that we always print the version field: // // ctx = WithLogger(ctx, GetLogger(ctx, "version")) // // Since the logger has been pushed to the context, we can now get the version // field for free with our log messages. Future calls to GetLogger on the new // context will have the version field: // // GetLogger(ctx).Infof("this log message has a version field") // // This becomes more powerful when we start stacking loggers. Let's say we // have the version logger from above but also want a request id. Using the // context above, in our request scoped function, we place another logger in // the context: // // ctx = context.WithValue(ctx, "http.request.id", "unique id") // called when building request context // ctx = WithLogger(ctx, GetLogger(ctx, "http.request.id")) // // When GetLogger is called on the new context, "http.request.id" will be // included as a logger field, along with the original "version" field: // // INFO[0000] this log message has a version field http.request.id=unique id version=v2.0.0-alpha.2.m // // Note that this only affects the new context, the previous context, with the // version field, can be used independently. Put another way, the new logger, // added to the request context, is unique to that context and can have // request scoped varaibles. // // HTTP Requests // // This package also contains several methods for working with http requests. // The concepts are very similar to those described above. We simply place the // request in the context using WithRequest. This makes the request variables // available. GetRequestLogger can then be called to get request specific // variables in a log line: // // ctx = WithRequest(ctx, req) // GetRequestLogger(ctx).Infof("request variables") // // Like above, if we want to include the request data in all log messages in // the context, we push the logger to a new context and use that one: // // ctx = WithLogger(ctx, GetRequestLogger(ctx)) // // The concept is fairly powerful and ensures that calls throughout the stack // can be traced in log messages. Using the fields like "http.request.id", one // can analyze call flow for a particular request with a simple grep of the // logs. package context docker-registry-2.6.2~ds1/context/http.go000066400000000000000000000217641313450123100204410ustar00rootroot00000000000000package context import ( "errors" "net" "net/http" "strings" "sync" "time" log "github.com/Sirupsen/logrus" "github.com/docker/distribution/uuid" "github.com/gorilla/mux" ) // Common errors used with this package. var ( ErrNoRequestContext = errors.New("no http request in context") ErrNoResponseWriterContext = errors.New("no http response in context") ) func parseIP(ipStr string) net.IP { ip := net.ParseIP(ipStr) if ip == nil { log.Warnf("invalid remote IP address: %q", ipStr) } return ip } // RemoteAddr extracts the remote address of the request, taking into // account proxy headers. func RemoteAddr(r *http.Request) string { if prior := r.Header.Get("X-Forwarded-For"); prior != "" { proxies := strings.Split(prior, ",") if len(proxies) > 0 { remoteAddr := strings.Trim(proxies[0], " ") if parseIP(remoteAddr) != nil { return remoteAddr } } } // X-Real-Ip is less supported, but worth checking in the // absence of X-Forwarded-For if realIP := r.Header.Get("X-Real-Ip"); realIP != "" { if parseIP(realIP) != nil { return realIP } } return r.RemoteAddr } // RemoteIP extracts the remote IP of the request, taking into // account proxy headers. func RemoteIP(r *http.Request) string { addr := RemoteAddr(r) // Try parsing it as "IP:port" if ip, _, err := net.SplitHostPort(addr); err == nil { return ip } return addr } // WithRequest places the request on the context. The context of the request // is assigned a unique id, available at "http.request.id". The request itself // is available at "http.request". Other common attributes are available under // the prefix "http.request.". If a request is already present on the context, // this method will panic. func WithRequest(ctx Context, r *http.Request) Context { if ctx.Value("http.request") != nil { // NOTE(stevvooe): This needs to be considered a programming error. It // is unlikely that we'd want to have more than one request in // context. panic("only one request per context") } return &httpRequestContext{ Context: ctx, startedAt: time.Now(), id: uuid.Generate().String(), r: r, } } // GetRequest returns the http request in the given context. Returns // ErrNoRequestContext if the context does not have an http request associated // with it. func GetRequest(ctx Context) (*http.Request, error) { if r, ok := ctx.Value("http.request").(*http.Request); r != nil && ok { return r, nil } return nil, ErrNoRequestContext } // GetRequestID attempts to resolve the current request id, if possible. An // error is return if it is not available on the context. func GetRequestID(ctx Context) string { return GetStringValue(ctx, "http.request.id") } // WithResponseWriter returns a new context and response writer that makes // interesting response statistics available within the context. func WithResponseWriter(ctx Context, w http.ResponseWriter) (Context, http.ResponseWriter) { if closeNotifier, ok := w.(http.CloseNotifier); ok { irwCN := &instrumentedResponseWriterCN{ instrumentedResponseWriter: instrumentedResponseWriter{ ResponseWriter: w, Context: ctx, }, CloseNotifier: closeNotifier, } return irwCN, irwCN } irw := instrumentedResponseWriter{ ResponseWriter: w, Context: ctx, } return &irw, &irw } // GetResponseWriter returns the http.ResponseWriter from the provided // context. If not present, ErrNoResponseWriterContext is returned. The // returned instance provides instrumentation in the context. func GetResponseWriter(ctx Context) (http.ResponseWriter, error) { v := ctx.Value("http.response") rw, ok := v.(http.ResponseWriter) if !ok || rw == nil { return nil, ErrNoResponseWriterContext } return rw, nil } // getVarsFromRequest let's us change request vars implementation for testing // and maybe future changes. var getVarsFromRequest = mux.Vars // WithVars extracts gorilla/mux vars and makes them available on the returned // context. Variables are available at keys with the prefix "vars.". For // example, if looking for the variable "name", it can be accessed as // "vars.name". Implementations that are accessing values need not know that // the underlying context is implemented with gorilla/mux vars. func WithVars(ctx Context, r *http.Request) Context { return &muxVarsContext{ Context: ctx, vars: getVarsFromRequest(r), } } // GetRequestLogger returns a logger that contains fields from the request in // the current context. If the request is not available in the context, no // fields will display. Request loggers can safely be pushed onto the context. func GetRequestLogger(ctx Context) Logger { return GetLogger(ctx, "http.request.id", "http.request.method", "http.request.host", "http.request.uri", "http.request.referer", "http.request.useragent", "http.request.remoteaddr", "http.request.contenttype") } // GetResponseLogger reads the current response stats and builds a logger. // Because the values are read at call time, pushing a logger returned from // this function on the context will lead to missing or invalid data. Only // call this at the end of a request, after the response has been written. func GetResponseLogger(ctx Context) Logger { l := getLogrusLogger(ctx, "http.response.written", "http.response.status", "http.response.contenttype") duration := Since(ctx, "http.request.startedat") if duration > 0 { l = l.WithField("http.response.duration", duration.String()) } return l } // httpRequestContext makes information about a request available to context. type httpRequestContext struct { Context startedAt time.Time id string r *http.Request } // Value returns a keyed element of the request for use in the context. To get // the request itself, query "request". For other components, access them as // "request.". For example, r.RequestURI func (ctx *httpRequestContext) Value(key interface{}) interface{} { if keyStr, ok := key.(string); ok { if keyStr == "http.request" { return ctx.r } if !strings.HasPrefix(keyStr, "http.request.") { goto fallback } parts := strings.Split(keyStr, ".") if len(parts) != 3 { goto fallback } switch parts[2] { case "uri": return ctx.r.RequestURI case "remoteaddr": return RemoteAddr(ctx.r) case "method": return ctx.r.Method case "host": return ctx.r.Host case "referer": referer := ctx.r.Referer() if referer != "" { return referer } case "useragent": return ctx.r.UserAgent() case "id": return ctx.id case "startedat": return ctx.startedAt case "contenttype": ct := ctx.r.Header.Get("Content-Type") if ct != "" { return ct } } } fallback: return ctx.Context.Value(key) } type muxVarsContext struct { Context vars map[string]string } func (ctx *muxVarsContext) Value(key interface{}) interface{} { if keyStr, ok := key.(string); ok { if keyStr == "vars" { return ctx.vars } if strings.HasPrefix(keyStr, "vars.") { keyStr = strings.TrimPrefix(keyStr, "vars.") } if v, ok := ctx.vars[keyStr]; ok { return v } } return ctx.Context.Value(key) } // instrumentedResponseWriterCN provides response writer information in a // context. It implements http.CloseNotifier so that users can detect // early disconnects. type instrumentedResponseWriterCN struct { instrumentedResponseWriter http.CloseNotifier } // instrumentedResponseWriter provides response writer information in a // context. This variant is only used in the case where CloseNotifier is not // implemented by the parent ResponseWriter. type instrumentedResponseWriter struct { http.ResponseWriter Context mu sync.Mutex status int written int64 } func (irw *instrumentedResponseWriter) Write(p []byte) (n int, err error) { n, err = irw.ResponseWriter.Write(p) irw.mu.Lock() irw.written += int64(n) // Guess the likely status if not set. if irw.status == 0 { irw.status = http.StatusOK } irw.mu.Unlock() return } func (irw *instrumentedResponseWriter) WriteHeader(status int) { irw.ResponseWriter.WriteHeader(status) irw.mu.Lock() irw.status = status irw.mu.Unlock() } func (irw *instrumentedResponseWriter) Flush() { if flusher, ok := irw.ResponseWriter.(http.Flusher); ok { flusher.Flush() } } func (irw *instrumentedResponseWriter) Value(key interface{}) interface{} { if keyStr, ok := key.(string); ok { if keyStr == "http.response" { return irw } if !strings.HasPrefix(keyStr, "http.response.") { goto fallback } parts := strings.Split(keyStr, ".") if len(parts) != 3 { goto fallback } irw.mu.Lock() defer irw.mu.Unlock() switch parts[2] { case "written": return irw.written case "status": return irw.status case "contenttype": contentType := irw.Header().Get("Content-Type") if contentType != "" { return contentType } } } fallback: return irw.Context.Value(key) } func (irw *instrumentedResponseWriterCN) Value(key interface{}) interface{} { if keyStr, ok := key.(string); ok { if keyStr == "http.response" { return irw } } return irw.instrumentedResponseWriter.Value(key) } docker-registry-2.6.2~ds1/context/http_test.go000066400000000000000000000144741313450123100215000ustar00rootroot00000000000000package context import ( "net/http" "net/http/httptest" "net/http/httputil" "net/url" "reflect" "testing" "time" ) func TestWithRequest(t *testing.T) { var req http.Request start := time.Now() req.Method = "GET" req.Host = "example.com" req.RequestURI = "/test-test" req.Header = make(http.Header) req.Header.Set("Referer", "foo.com/referer") req.Header.Set("User-Agent", "test/0.1") ctx := WithRequest(Background(), &req) for _, testcase := range []struct { key string expected interface{} }{ { key: "http.request", expected: &req, }, { key: "http.request.id", }, { key: "http.request.method", expected: req.Method, }, { key: "http.request.host", expected: req.Host, }, { key: "http.request.uri", expected: req.RequestURI, }, { key: "http.request.referer", expected: req.Referer(), }, { key: "http.request.useragent", expected: req.UserAgent(), }, { key: "http.request.remoteaddr", expected: req.RemoteAddr, }, { key: "http.request.startedat", }, } { v := ctx.Value(testcase.key) if v == nil { t.Fatalf("value not found for %q", testcase.key) } if testcase.expected != nil && v != testcase.expected { t.Fatalf("%s: %v != %v", testcase.key, v, testcase.expected) } // Key specific checks! switch testcase.key { case "http.request.id": if _, ok := v.(string); !ok { t.Fatalf("request id not a string: %v", v) } case "http.request.startedat": vt, ok := v.(time.Time) if !ok { t.Fatalf("value not a time: %v", v) } now := time.Now() if vt.After(now) { t.Fatalf("time generated too late: %v > %v", vt, now) } if vt.Before(start) { t.Fatalf("time generated too early: %v < %v", vt, start) } } } } type testResponseWriter struct { flushed bool status int written int64 header http.Header } func (trw *testResponseWriter) Header() http.Header { if trw.header == nil { trw.header = make(http.Header) } return trw.header } func (trw *testResponseWriter) Write(p []byte) (n int, err error) { if trw.status == 0 { trw.status = http.StatusOK } n = len(p) trw.written += int64(n) return } func (trw *testResponseWriter) WriteHeader(status int) { trw.status = status } func (trw *testResponseWriter) Flush() { trw.flushed = true } func TestWithResponseWriter(t *testing.T) { trw := testResponseWriter{} ctx, rw := WithResponseWriter(Background(), &trw) if ctx.Value("http.response") != rw { t.Fatalf("response not available in context: %v != %v", ctx.Value("http.response"), rw) } grw, err := GetResponseWriter(ctx) if err != nil { t.Fatalf("error getting response writer: %v", err) } if grw != rw { t.Fatalf("unexpected response writer returned: %#v != %#v", grw, rw) } if ctx.Value("http.response.status") != 0 { t.Fatalf("response status should always be a number and should be zero here: %v != 0", ctx.Value("http.response.status")) } if n, err := rw.Write(make([]byte, 1024)); err != nil { t.Fatalf("unexpected error writing: %v", err) } else if n != 1024 { t.Fatalf("unexpected number of bytes written: %v != %v", n, 1024) } if ctx.Value("http.response.status") != http.StatusOK { t.Fatalf("unexpected response status in context: %v != %v", ctx.Value("http.response.status"), http.StatusOK) } if ctx.Value("http.response.written") != int64(1024) { t.Fatalf("unexpected number reported bytes written: %v != %v", ctx.Value("http.response.written"), 1024) } // Make sure flush propagates rw.(http.Flusher).Flush() if !trw.flushed { t.Fatalf("response writer not flushed") } // Write another status and make sure context is correct. This normally // wouldn't work except for in this contrived testcase. rw.WriteHeader(http.StatusBadRequest) if ctx.Value("http.response.status") != http.StatusBadRequest { t.Fatalf("unexpected response status in context: %v != %v", ctx.Value("http.response.status"), http.StatusBadRequest) } } func TestWithVars(t *testing.T) { var req http.Request vars := map[string]string{ "foo": "asdf", "bar": "qwer", } getVarsFromRequest = func(r *http.Request) map[string]string { if r != &req { t.Fatalf("unexpected request: %v != %v", r, req) } return vars } ctx := WithVars(Background(), &req) for _, testcase := range []struct { key string expected interface{} }{ { key: "vars", expected: vars, }, { key: "vars.foo", expected: "asdf", }, { key: "vars.bar", expected: "qwer", }, } { v := ctx.Value(testcase.key) if !reflect.DeepEqual(v, testcase.expected) { t.Fatalf("%q: %v != %v", testcase.key, v, testcase.expected) } } } // SingleHostReverseProxy will insert an X-Forwarded-For header, and can be used to test // RemoteAddr(). A fake RemoteAddr cannot be set on the HTTP request - it is overwritten // at the transport layer to 127.0.0.1: . However, as the X-Forwarded-For header // just contains the IP address, it is different enough for testing. func TestRemoteAddr(t *testing.T) { var expectedRemote string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if r.RemoteAddr == expectedRemote { t.Errorf("Unexpected matching remote addresses") } actualRemote := RemoteAddr(r) if expectedRemote != actualRemote { t.Errorf("Mismatching remote hosts: %v != %v", expectedRemote, actualRemote) } w.WriteHeader(200) })) defer backend.Close() backendURL, err := url.Parse(backend.URL) if err != nil { t.Fatal(err) } proxy := httputil.NewSingleHostReverseProxy(backendURL) frontend := httptest.NewServer(proxy) defer frontend.Close() // X-Forwarded-For set by proxy expectedRemote = "127.0.0.1" proxyReq, err := http.NewRequest("GET", frontend.URL, nil) if err != nil { t.Fatal(err) } _, err = http.DefaultClient.Do(proxyReq) if err != nil { t.Fatal(err) } // RemoteAddr in X-Real-Ip getReq, err := http.NewRequest("GET", backend.URL, nil) if err != nil { t.Fatal(err) } expectedRemote = "1.2.3.4" getReq.Header["X-Real-ip"] = []string{expectedRemote} _, err = http.DefaultClient.Do(getReq) if err != nil { t.Fatal(err) } // Valid X-Real-Ip and invalid X-Forwarded-For getReq.Header["X-forwarded-for"] = []string{"1.2.3"} _, err = http.DefaultClient.Do(getReq) if err != nil { t.Fatal(err) } } docker-registry-2.6.2~ds1/context/logger.go000066400000000000000000000067351313450123100207420ustar00rootroot00000000000000package context import ( "fmt" "github.com/Sirupsen/logrus" "runtime" ) // Logger provides a leveled-logging interface. type Logger interface { // standard logger methods Print(args ...interface{}) Printf(format string, args ...interface{}) Println(args ...interface{}) Fatal(args ...interface{}) Fatalf(format string, args ...interface{}) Fatalln(args ...interface{}) Panic(args ...interface{}) Panicf(format string, args ...interface{}) Panicln(args ...interface{}) // Leveled methods, from logrus Debug(args ...interface{}) Debugf(format string, args ...interface{}) Debugln(args ...interface{}) Error(args ...interface{}) Errorf(format string, args ...interface{}) Errorln(args ...interface{}) Info(args ...interface{}) Infof(format string, args ...interface{}) Infoln(args ...interface{}) Warn(args ...interface{}) Warnf(format string, args ...interface{}) Warnln(args ...interface{}) } // WithLogger creates a new context with provided logger. func WithLogger(ctx Context, logger Logger) Context { return WithValue(ctx, "logger", logger) } // GetLoggerWithField returns a logger instance with the specified field key // and value without affecting the context. Extra specified keys will be // resolved from the context. func GetLoggerWithField(ctx Context, key, value interface{}, keys ...interface{}) Logger { return getLogrusLogger(ctx, keys...).WithField(fmt.Sprint(key), value) } // GetLoggerWithFields returns a logger instance with the specified fields // without affecting the context. Extra specified keys will be resolved from // the context. func GetLoggerWithFields(ctx Context, fields map[interface{}]interface{}, keys ...interface{}) Logger { // must convert from interface{} -> interface{} to string -> interface{} for logrus. lfields := make(logrus.Fields, len(fields)) for key, value := range fields { lfields[fmt.Sprint(key)] = value } return getLogrusLogger(ctx, keys...).WithFields(lfields) } // GetLogger returns the logger from the current context, if present. If one // or more keys are provided, they will be resolved on the context and // included in the logger. While context.Value takes an interface, any key // argument passed to GetLogger will be passed to fmt.Sprint when expanded as // a logging key field. If context keys are integer constants, for example, // its recommended that a String method is implemented. func GetLogger(ctx Context, keys ...interface{}) Logger { return getLogrusLogger(ctx, keys...) } // GetLogrusLogger returns the logrus logger for the context. If one more keys // are provided, they will be resolved on the context and included in the // logger. Only use this function if specific logrus functionality is // required. func getLogrusLogger(ctx Context, keys ...interface{}) *logrus.Entry { var logger *logrus.Entry // Get a logger, if it is present. loggerInterface := ctx.Value("logger") if loggerInterface != nil { if lgr, ok := loggerInterface.(*logrus.Entry); ok { logger = lgr } } if logger == nil { fields := logrus.Fields{} // Fill in the instance id, if we have it. instanceID := ctx.Value("instance.id") if instanceID != nil { fields["instance.id"] = instanceID } fields["go.version"] = runtime.Version() // If no logger is found, just return the standard logger. logger = logrus.StandardLogger().WithFields(fields) } fields := logrus.Fields{} for _, key := range keys { v := ctx.Value(key) if v != nil { fields[fmt.Sprint(key)] = v } } return logger.WithFields(fields) } docker-registry-2.6.2~ds1/context/trace.go000066400000000000000000000054511313450123100205530ustar00rootroot00000000000000package context import ( "runtime" "time" "github.com/docker/distribution/uuid" ) // WithTrace allocates a traced timing span in a new context. This allows a // caller to track the time between calling WithTrace and the returned done // function. When the done function is called, a log message is emitted with a // "trace.duration" field, corresponding to the elapsed time and a // "trace.func" field, corresponding to the function that called WithTrace. // // The logging keys "trace.id" and "trace.parent.id" are provided to implement // dapper-like tracing. This function should be complemented with a WithSpan // method that could be used for tracing distributed RPC calls. // // The main benefit of this function is to post-process log messages or // intercept them in a hook to provide timing data. Trace ids and parent ids // can also be linked to provide call tracing, if so required. // // Here is an example of the usage: // // func timedOperation(ctx Context) { // ctx, done := WithTrace(ctx) // defer done("this will be the log message") // // ... function body ... // } // // If the function ran for roughly 1s, such a usage would emit a log message // as follows: // // INFO[0001] this will be the log message trace.duration=1.004575763s trace.func=github.com/docker/distribution/context.traceOperation trace.id= ... // // Notice that the function name is automatically resolved, along with the // package and a trace id is emitted that can be linked with parent ids. func WithTrace(ctx Context) (Context, func(format string, a ...interface{})) { if ctx == nil { ctx = Background() } pc, file, line, _ := runtime.Caller(1) f := runtime.FuncForPC(pc) ctx = &traced{ Context: ctx, id: uuid.Generate().String(), start: time.Now(), parent: GetStringValue(ctx, "trace.id"), fnname: f.Name(), file: file, line: line, } return ctx, func(format string, a ...interface{}) { GetLogger(ctx, "trace.duration", "trace.id", "trace.parent.id", "trace.func", "trace.file", "trace.line"). Debugf(format, a...) } } // traced represents a context that is traced for function call timing. It // also provides fast lookup for the various attributes that are available on // the trace. type traced struct { Context id string parent string start time.Time fnname string file string line int } func (ts *traced) Value(key interface{}) interface{} { switch key { case "trace.start": return ts.start case "trace.duration": return time.Since(ts.start) case "trace.id": return ts.id case "trace.parent.id": if ts.parent == "" { return nil // must return nil to signal no parent. } return ts.parent case "trace.func": return ts.fnname case "trace.file": return ts.file case "trace.line": return ts.line } return ts.Context.Value(key) } docker-registry-2.6.2~ds1/context/trace_test.go000066400000000000000000000035161313450123100216120ustar00rootroot00000000000000package context import ( "runtime" "testing" "time" ) // TestWithTrace ensures that tracing has the expected values in the context. func TestWithTrace(t *testing.T) { pc, file, _, _ := runtime.Caller(0) // get current caller. f := runtime.FuncForPC(pc) base := []valueTestCase{ { key: "trace.id", notnilorempty: true, }, { key: "trace.file", expected: file, notnilorempty: true, }, { key: "trace.line", notnilorempty: true, }, { key: "trace.start", notnilorempty: true, }, } ctx, done := WithTrace(Background()) defer done("this will be emitted at end of test") checkContextForValues(t, ctx, append(base, valueTestCase{ key: "trace.func", expected: f.Name(), })) traced := func() { parentID := ctx.Value("trace.id") // ensure the parent trace id is correct. pc, _, _, _ := runtime.Caller(0) // get current caller. f := runtime.FuncForPC(pc) ctx, done := WithTrace(ctx) defer done("this should be subordinate to the other trace") time.Sleep(time.Second) checkContextForValues(t, ctx, append(base, valueTestCase{ key: "trace.func", expected: f.Name(), }, valueTestCase{ key: "trace.parent.id", expected: parentID, })) } traced() time.Sleep(time.Second) } type valueTestCase struct { key string expected interface{} notnilorempty bool // just check not empty/not nil } func checkContextForValues(t *testing.T, ctx Context, values []valueTestCase) { for _, testcase := range values { v := ctx.Value(testcase.key) if testcase.notnilorempty { if v == nil || v == "" { t.Fatalf("value was nil or empty for %q: %#v", testcase.key, v) } continue } if v != testcase.expected { t.Fatalf("unexpected value for key %q: %v != %v", testcase.key, v, testcase.expected) } } } docker-registry-2.6.2~ds1/context/util.go000066400000000000000000000012611313450123100204250ustar00rootroot00000000000000package context import ( "time" ) // Since looks up key, which should be a time.Time, and returns the duration // since that time. If the key is not found, the value returned will be zero. // This is helpful when inferring metrics related to context execution times. func Since(ctx Context, key interface{}) time.Duration { if startedAt, ok := ctx.Value(key).(time.Time); ok { return time.Since(startedAt) } return 0 } // GetStringValue returns a string value from the context. The empty string // will be returned if not found. func GetStringValue(ctx Context, key interface{}) (value string) { if valuev, ok := ctx.Value(key).(string); ok { value = valuev } return value } docker-registry-2.6.2~ds1/context/version.go000066400000000000000000000011121313450123100211300ustar00rootroot00000000000000package context // WithVersion stores the application version in the context. The new context // gets a logger to ensure log messages are marked with the application // version. func WithVersion(ctx Context, version string) Context { ctx = WithValue(ctx, "version", version) // push a new logger onto the stack return WithLogger(ctx, GetLogger(ctx, "version")) } // GetVersion returns the application version from the context. An empty // string may returned if the version was not set on the context. func GetVersion(ctx Context) string { return GetStringValue(ctx, "version") } docker-registry-2.6.2~ds1/context/version_test.go000066400000000000000000000005561313450123100222020ustar00rootroot00000000000000package context import "testing" func TestVersionContext(t *testing.T) { ctx := Background() if GetVersion(ctx) != "" { t.Fatalf("context should not yet have a version") } expected := "2.1-whatever" ctx = WithVersion(ctx, expected) version := GetVersion(ctx) if version != expected { t.Fatalf("version was not set: %q != %q", version, expected) } } docker-registry-2.6.2~ds1/contrib/000077500000000000000000000000001313450123100170755ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/apache/000077500000000000000000000000001313450123100203165ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/apache/README.MD000066400000000000000000000024571313450123100215050ustar00rootroot00000000000000# Apache HTTPd sample for Registry v1, v2 and mirror 3 containers involved * Docker Registry v1 (registry 0.9.1) * Docker Registry v2 (registry 2.0.0) * Docker Registry v1 in mirror mode HTTP for mirror and HTTPS for v1 & v2 * http://registry.example.com proxify Docker Registry 1.0 in Mirror mode * https://registry.example.com proxify Docker Registry 1.0 or 2.0 in Hosting mode ## 3 Docker containers should be started * Docker Registry 1.0 in Mirror mode : port 5001 * Docker Registry 1.0 in Hosting mode : port 5000 * Docker Registry 2.0 in Hosting mode : port 5002 ### Registry v1 docker run -d -e SETTINGS_FLAVOR=dev -v /var/lib/docker-registry/storage/hosting-v1:/tmp -p 5000:5000 registry:0.9.1" ### Mirror docker run -d -e SETTINGS_FLAVOR=dev -e STANDALONE=false -e MIRROR_SOURCE=https://registry-1.docker.io -e MIRROR_SOURCE_INDEX=https://index.docker.io \ -e MIRROR_TAGS_CACHE_TTL=172800 -v /var/lib/docker-registry/storage/mirror:/tmp -p 5001:5000 registry:0.9.1" ### Registry v2 docker run -d -e SETTINGS_FLAVOR=dev -v /var/lib/axway/docker-registry/storage/hosting2-v2:/tmp -p 5002:5000 registry:2" # For Hosting mode access * users should have account (valid-user) to be able to fetch images * only users using account docker-deployer will be allowed to push images docker-registry-2.6.2~ds1/contrib/apache/apache.conf000066400000000000000000000055311313450123100224120ustar00rootroot00000000000000# # Sample Apache 2.x configuration where : # ServerName registry.example.com ServerAlias www.registry.example.com ProxyRequests off ProxyPreserveHost on # no proxy for /error/ (Apache HTTPd errors messages) ProxyPass /error/ ! ProxyPass /_ping http://localhost:5001/_ping ProxyPassReverse /_ping http://localhost:5001/_ping ProxyPass /v1 http://localhost:5001/v1 ProxyPassReverse /v1 http://localhost:5001/v1 # Logs ErrorLog ${APACHE_LOG_DIR}/mirror_error_log CustomLog ${APACHE_LOG_DIR}/mirror_access_log combined env=!dontlog ServerName registry.example.com ServerAlias www.registry.example.com SSLEngine on SSLCertificateFile /etc/apache2/ssl/registry.example.com.crt SSLCertificateKeyFile /etc/apache2/ssl/registry.example.com.key # Higher Strength SSL Ciphers SSLProtocol all -SSLv2 -SSLv3 -TLSv1 SSLCipherSuite RC4-SHA:HIGH SSLHonorCipherOrder on # Logs ErrorLog ${APACHE_LOG_DIR}/registry_error_ssl_log CustomLog ${APACHE_LOG_DIR}/registry_access_ssl_log combined env=!dontlog Header always set "Docker-Distribution-Api-Version" "registry/2.0" Header onsuccess set "Docker-Distribution-Api-Version" "registry/2.0" RequestHeader set X-Forwarded-Proto "https" ProxyRequests off ProxyPreserveHost on # no proxy for /error/ (Apache HTTPd errors messages) ProxyPass /error/ ! # # Registry v1 # ProxyPass /v1 http://localhost:5000/v1 ProxyPassReverse /v1 http://localhost:5000/v1 ProxyPass /_ping http://localhost:5000/_ping ProxyPassReverse /_ping http://localhost:5000/_ping # Authentication require for push Order deny,allow Allow from all AuthName "Registry Authentication" AuthType basic AuthUserFile "/etc/apache2/htpasswd/registry-htpasswd" # Read access to authentified users Require valid-user # Write access to docker-deployer account only Require user docker-deployer # Allow ping to run unauthenticated. Satisfy any Allow from all # Allow ping to run unauthenticated. Satisfy any Allow from all # # Registry v2 # ProxyPass /v2 http://localhost:5002/v2 ProxyPassReverse /v2 http://localhost:5002/v2 Order deny,allow Allow from all AuthName "Registry Authentication" AuthType basic AuthUserFile "/etc/apache2/htpasswd/registry-htpasswd" # Read access to authentified users Require valid-user # Write access to docker-deployer only Require user docker-deployer docker-registry-2.6.2~ds1/contrib/compose/000077500000000000000000000000001313450123100205425ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/compose/README.md000066400000000000000000000116361313450123100220300ustar00rootroot00000000000000# Docker Compose V1 + V2 registry This compose configuration configures a `v1` and `v2` registry behind an `nginx` proxy. By default, you can access the combined registry at `localhost:5000`. The configuration does not support pushing images to `v2` and pulling from `v1`. If a `docker` client has a version less than 1.6, Nginx will route its requests to the 1.0 registry. Requests from newer clients will route to the 2.0 registry. ### Install Docker Compose 1. Open a new terminal on the host with your `distribution` source. 2. Get the `docker-compose` binary. $ sudo wget https://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname -s`-`uname -m` -O /usr/local/bin/docker-compose This command installs the binary in the `/usr/local/bin` directory. 3. Add executable permissions to the binary. $ sudo chmod +x /usr/local/bin/docker-compose ## Build and run with Compose 1. In your terminal, navigate to the `distribution/contrib/compose` directory This directory includes a single `docker-compose.yml` configuration. nginx: build: "nginx" ports: - "5000:5000" links: - registryv1:registryv1 - registryv2:registryv2 registryv1: image: registry ports: - "5000" registryv2: build: "../../" ports: - "5000" This configuration builds a new `nginx` image as specified by the `nginx/Dockerfile` file. The 1.0 registry comes from Docker's official public image. Finally, the registry 2.0 image is built from the `distribution/Dockerfile` you've used previously. 2. Get a registry 1.0 image. $ docker pull registry:0.9.1 The Compose configuration looks for this image locally. If you don't do this step, later steps can fail. 3. Build `nginx`, the registry 2.0 image, and $ docker-compose build registryv1 uses an image, skipping Building registryv2... Step 0 : FROM golang:1.4 ... Removing intermediate container 9f5f5068c3f3 Step 4 : COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf ---> 74acc70fa106 Removing intermediate container edb84c2b40cb Successfully built 74acc70fa106 The commmand outputs its progress until it completes. 4. Start your configuration with compose. $ docker-compose up Recreating compose_registryv1_1... Recreating compose_registryv2_1... Recreating compose_nginx_1... Attaching to compose_registryv1_1, compose_registryv2_1, compose_nginx_1 ... 5. In another terminal, display the running configuration. $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a81ad2557702 compose_nginx:latest "nginx -g 'daemon of 8 minutes ago Up 8 minutes 80/tcp, 443/tcp, 0.0.0.0:5000->5000/tcp compose_nginx_1 0618437450dd compose_registryv2:latest "registry cmd/regist 8 minutes ago Up 8 minutes 0.0.0.0:32777->5000/tcp compose_registryv2_1 aa82b1ed8e61 registry:latest "docker-registry" 8 minutes ago Up 8 minutes 0.0.0.0:32776->5000/tcp compose_registryv1_1 ### Explore a bit 1. Check for TLS on your `nginx` server. $ curl -v https://localhost:5000 * Rebuilt URL to: https://localhost:5000/ * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 5000 (#0) * successfully set certificate verify locations: * CAfile: none CApath: /etc/ssl/certs * SSLv3, TLS handshake, Client hello (1): * SSLv3, TLS handshake, Server hello (2): * SSLv3, TLS handshake, CERT (11): * SSLv3, TLS alert, Server hello (2): * SSL certificate problem: self signed certificate * Closing connection 0 curl: (60) SSL certificate problem: self signed certificate More details here: http://curl.haxx.se/docs/sslcerts.html 2. Tag the `v1` registry image. $ docker tag registry:latest localhost:5000/registry_one:latest 2. Push it to the localhost. $ docker push localhost:5000/registry_one:latest If you are using the 1.6 Docker client, this pushes the image the `v2 `registry. 4. Use `curl` to list the image in the registry. $ curl -v -X GET http://localhost:32777/v2/registry1/tags/list * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to localhost (127.0.0.1) port 32777 (#0) > GET /v2/registry1/tags/list HTTP/1.1 > User-Agent: curl/7.36.0 > Host: localhost:32777 > Accept: */* > < HTTP/1.1 200 OK < Content-Type: application/json; charset=utf-8 < Docker-Distribution-Api-Version: registry/2.0 < Date: Tue, 14 Apr 2015 22:34:13 GMT < Content-Length: 39 < {"name":"registry1","tags":["latest"]} * Connection #0 to host localhost left intact This example refers to the specific port assigned to the 2.0 registry. You saw this port earlier, when you used `docker ps` to show your running containers. docker-registry-2.6.2~ds1/contrib/compose/docker-compose.yml000066400000000000000000000003341313450123100241770ustar00rootroot00000000000000nginx: build: "nginx" ports: - "5000:5000" links: - registryv1:registryv1 - registryv2:registryv2 registryv1: image: registry ports: - "5000" registryv2: build: "../../" ports: - "5000" docker-registry-2.6.2~ds1/contrib/compose/nginx/000077500000000000000000000000001313450123100216655ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/compose/nginx/Dockerfile000066400000000000000000000003431313450123100236570ustar00rootroot00000000000000FROM nginx:1.7 COPY nginx.conf /etc/nginx/nginx.conf COPY registry.conf /etc/nginx/conf.d/registry.conf COPY docker-registry.conf /etc/nginx/docker-registry.conf COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf docker-registry-2.6.2~ds1/contrib/compose/nginx/docker-registry-v2.conf000066400000000000000000000005701313450123100262000ustar00rootroot00000000000000proxy_pass http://docker-registry-v2; proxy_set_header Host $http_host; # required for docker client's sake proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 900; docker-registry-2.6.2~ds1/contrib/compose/nginx/docker-registry.conf000066400000000000000000000007441313450123100256560ustar00rootroot00000000000000proxy_pass http://docker-registry; proxy_set_header Host $http_host; # required for docker client's sake proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Authorization ""; # For basic auth through nginx in v1 to work, please comment this line proxy_read_timeout 900; docker-registry-2.6.2~ds1/contrib/compose/nginx/nginx.conf000066400000000000000000000011271313450123100236600ustar00rootroot00000000000000user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; } docker-registry-2.6.2~ds1/contrib/compose/nginx/registry.conf000066400000000000000000000021041313450123100244010ustar00rootroot00000000000000# Docker registry proxy for api versions 1 and 2 upstream docker-registry { server registryv1:5000; } upstream docker-registry-v2 { server registryv2:5000; } # No client auth or TLS server { listen 5000; server_name localhost; # disable any limits to avoid HTTP 413 for large image uploads client_max_body_size 0; # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) chunked_transfer_encoding on; location /v2/ { # Do not allow connections from docker 1.5 and earlier # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) { return 404; } # To add basic authentication to v2 use auth_basic setting plus add_header # auth_basic "registry.localhost"; # auth_basic_user_file test.password; # add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always; include docker-registry-v2.conf; } location / { include docker-registry.conf; } } docker-registry-2.6.2~ds1/contrib/docker-integration/000077500000000000000000000000001313450123100226655ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/Dockerfile000066400000000000000000000003421313450123100246560ustar00rootroot00000000000000FROM distribution/golem:0.1 MAINTAINER Docker Distribution Team RUN apk add --no-cache git ENV TMPDIR /var/lib/docker/tmp WORKDIR /go/src/github.com/docker/distribution/contrib/docker-integration docker-registry-2.6.2~ds1/contrib/docker-integration/README.md000066400000000000000000000047561313450123100241600ustar00rootroot00000000000000# Docker Registry Integration Testing These integration tests cover interactions between registry clients such as the docker daemon and the registry server. All tests can be run using the [golem integration test runner](https://github.com/docker/golem) The integration tests configure components using docker compose (see docker-compose.yaml) and the runner can be using the golem configuration file (see golem.conf). ## Running integration tests ### Run using multiversion script The integration tests in the `contrib/docker-integration` directory can be simply run by executing the run script `./run_multiversion.sh`. If there is no running daemon to connect to, run as `./run_multiversion.sh -d`. This command will build the distribution image from the locally checked out version and run against multiple versions of docker defined in the script. To run a specific version of the registry or docker, Golem will need to be executed manually. ### Run manually using Golem Using the golem tool directly allows running against multiple versions of the registry and docker. Running against multiple versions of the registry can be useful for testing changes in the docker daemon which are not covered by the default run script. #### Installing Golem Golem is distributed as an executable binary which can be installed from the [release page](https://github.com/docker/golem/releases/tag/v0.1). #### Running golem with docker Additionally golem can be run as a docker image requiring no additonal installation. `docker run --privileged -v "$GOPATH/src/github.com/docker/distribution/contrib/docker-integration:/test" -w /test distribution/golem golem -rundaemon .` #### Golem custom images Golem tests version of software by defining the docker image to test. Run with registry 2.2.1 and docker 1.10.3 `golem -i golem-dind:latest,docker:1.10.3-dind,1.10.3 -i golem-distribution:latest,registry:2.2.1 .` #### Use golem caching for developing tests Golem allows caching image configuration to reduce test start up time. Using this cache will allow tests with the same set of images to start up quickly. This can be useful when developing tests and needing the test to run quickly. If there are changes which effect the image (such as building a new registry image), then startup time will be slower. Run this command multiple times and after the first time test runs should start much quicker. `golem -cache ~/.cache/docker/golem -i golem-dind:latest,docker:1.10.3-dind,1.10.3 -i golem-distribution:latest,registry:2.2.1 .` docker-registry-2.6.2~ds1/contrib/docker-integration/docker-compose.yml000066400000000000000000000054641313450123100263330ustar00rootroot00000000000000nginx: build: "nginx" ports: - "5000:5000" - "5002:5002" - "5440:5440" - "5441:5441" - "5442:5442" - "5443:5443" - "5444:5444" - "5445:5445" - "5446:5446" - "5447:5447" - "5448:5448" - "5554:5554" - "5555:5555" - "5556:5556" - "5557:5557" - "5558:5558" - "5559:5559" - "5600:5600" - "6666:6666" links: - registryv2:registryv2 - malevolent:malevolent - registryv2token:registryv2token - tokenserver:tokenserver - registryv2tokenoauth:registryv2tokenoauth - registryv2tokenoauthnotls:registryv2tokenoauthnotls - tokenserveroauth:tokenserveroauth registryv2: image: golem-distribution:latest ports: - "5000" registryv2token: image: golem-distribution:latest ports: - "5000" volumes: - ./tokenserver/registry-config.yml:/etc/docker/registry/config.yml - ./tokenserver/certs/localregistry.cert:/etc/docker/registry/localregistry.cert - ./tokenserver/certs/localregistry.key:/etc/docker/registry/localregistry.key - ./tokenserver/certs/signing.cert:/etc/docker/registry/tokenbundle.pem tokenserver: build: "tokenserver" command: "--debug -addr 0.0.0.0:5556 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5556" ports: - "5556" registryv2tokenoauth: image: golem-distribution:latest ports: - "5000" volumes: - ./tokenserver-oauth/registry-config.yml:/etc/docker/registry/config.yml - ./tokenserver-oauth/certs/localregistry.cert:/etc/docker/registry/localregistry.cert - ./tokenserver-oauth/certs/localregistry.key:/etc/docker/registry/localregistry.key - ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem registryv2tokenoauthnotls: image: golem-distribution:latest ports: - "5000" volumes: - ./tokenserver-oauth/registry-config-notls.yml:/etc/docker/registry/config.yml - ./tokenserver-oauth/certs/signing.cert:/etc/docker/registry/tokenbundle.pem tokenserveroauth: build: "tokenserver-oauth" command: "--debug -addr 0.0.0.0:5559 -issuer registry-test -passwd .htpasswd -tlscert tls.cert -tlskey tls.key -key sign.key -realm http://auth.localregistry:5559" ports: - "5559" malevolent: image: "dmcgowan/malevolent:0.1.0" command: "-l 0.0.0.0:6666 -r http://registryv2:5000 -c /certs/localregistry.cert -k /certs/localregistry.key" links: - registryv2:registryv2 volumes: - ./malevolent-certs:/certs:ro ports: - "6666" docker: image: golem-dind:latest container_name: dockerdaemon command: "docker daemon --debug -s $DOCKER_GRAPHDRIVER" privileged: true environment: DOCKER_GRAPHDRIVER: volumes: - /etc/generated_certs.d:/etc/docker/certs.d - /var/lib/docker links: - nginx:localregistry - nginx:auth.localregistry docker-registry-2.6.2~ds1/contrib/docker-integration/golem.conf000066400000000000000000000011761313450123100246440ustar00rootroot00000000000000[[suite]] dind=true images=[ "nginx:1.9", "dmcgowan/token-server:simple", "dmcgowan/token-server:oauth", "dmcgowan/malevolent:0.1.0" ] [[suite.pretest]] command="sh ./install_certs.sh /etc/generated_certs.d" [[suite.testrunner]] command="bats -t ." format="tap" env=["TEST_REPO=hello-world", "TEST_TAG=latest", "TEST_USER=testuser", "TEST_PASSWORD=passpassword", "TEST_REGISTRY=localregistry", "TEST_SKIP_PULL=true"] [[suite.customimage]] tag="golem-distribution:latest" default="registry:2.2.1" [[suite.customimage]] tag="golem-dind:latest" default="docker:1.10.1-dind" version="1.10.1" docker-registry-2.6.2~ds1/contrib/docker-integration/helpers.bash000066400000000000000000000052161313450123100251720ustar00rootroot00000000000000# has_digest enforces the last output line is "Digest: sha256:..." # the input is the output from a docker push cli command function has_digest() { filtered=$(echo "$1" |sed -rn '/[dD]igest\: sha(256|384|512)/ p') [ "$filtered" != "" ] # See http://wiki.alpinelinux.org/wiki/Regex#BREs before making changes to regex digest=$(expr "$filtered" : ".*\(sha[0-9]\{3,3\}:[a-z0-9]*\)") } # tempImage creates a new image using the provided name # requires bats function tempImage() { dir=$(mktemp -d) run dd if=/dev/urandom of="$dir/f" bs=1024 count=512 cat < "$dir/Dockerfile" FROM scratch COPY f /f CMD [] DockerFileContent cp_t $dir "/tmpbuild/" exec_t "cd /tmpbuild/; docker build --no-cache -t $1 .; rm -rf /tmpbuild/" } # skip basic auth tests with Docker 1.6, where they don't pass due to # certificate issues, requires bats function basic_auth_version_check() { run sh -c 'docker version | fgrep -q "Client version: 1.6."' if [ "$status" -eq 0 ]; then skip "Basic auth tests don't support 1.6.x" fi } # login issues a login to docker to the provided server # uses user, password, and email variables set outside of function # requies bats function login() { rm -f /root/.docker/config.json run docker_t login -u $user -p $password -e $email $1 if [ "$status" -ne 0 ]; then echo $output fi [ "$status" -eq 0 ] # First line is WARNING about credential save or email deprecation (maybe both) [ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ] } function login_oauth() { login $@ tmpFile=$(mktemp) get_file_t /root/.docker/config.json $tmpFile run awk -v RS="" "/\"$1\": \\{[[:space:]]+\"auth\": \"[[:alnum:]]+\",[[:space:]]+\"identitytoken\"/ {exit 3}" $tmpFile [ "$status" -eq 3 ] } function parse_version() { version=$(echo "$1" | cut -d '-' -f1) # Strip anything after '-' major=$(echo "$version" | cut -d . -f1) minor=$(echo "$version" | cut -d . -f2) rev=$(echo "$version" | cut -d . -f3) version=$((major * 1000 * 1000 + minor * 1000 + rev)) } function version_check() { name=$1 checkv=$2 minv=$3 parse_version "$checkv" v=$version parse_version "$minv" if [ "$v" -lt "$version" ]; then skip "$name version \"$checkv\" does not meet required version \"$minv\"" fi } function get_file_t() { docker cp dockerdaemon:$1 $2 } function cp_t() { docker cp $1 dockerdaemon:$2 } function exec_t() { docker exec dockerdaemon sh -c "$@" } function docker_t() { docker exec dockerdaemon docker $@ } # build reates a new docker image id from another image function build() { docker exec -i dockerdaemon docker build --no-cache -t $1 - <> $2/ca.crt } install_test_certs $installdir # Malevolent server install_ca_file ./malevolent-certs/ca.pem $installdir/$hostname:6666 # Token server install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5554 install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5555 install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5557 install_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5558 append_ca_file ./tokenserver/certs/ca.pem $installdir/$hostname:5600 docker-registry-2.6.2~ds1/contrib/docker-integration/malevolent-certs/000077500000000000000000000000001313450123100261515ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/malevolent-certs/ca.pem000066400000000000000000000020761313450123100272440ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIQKQTGjKpSVBW78ef0fOcxRTALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDgyMDIz MjE0OVoXDTE4MDgwNDIzMjE0OVowJjERMA8GA1UEChMIUXVpY2tUTFMxETAPBgNV BAMTCFF1aWNrVExTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwoPM xiDZK6Fwy5r3waRkfJHhyZZH828Jyj+nz5UVkMyOM/xN6MgJ2w911hTj1wSXG2n3 AohF3gTFNrDYh4j2qRZnixDrOM5GBm2/KJbyfBIYkrR45yLfjidO7MRnhaPZ5Fov l+RKwNBXP4Q2mUe7q9FM457Rm8hAcqXP04AJT20m1QSYQivDgxsDxuAQte3VEy1E 0j0CwUKoFHT6MHOnDPEZbc4r1+ba34WBM1Sc5KXyV2JlbtU07J4hACYWVsD7vQCl VFlZNE4E35ahMDZ+ODLal9PAT8ARLdAtjvRWrT+h8qZ4Yfwt/sGF1K4CAkTP3H5p uMkJG56zmqIEYeHMuwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/ BAUwAwEB/zALBgkqhkiG9w0BAQsDggEBALpieTckiPEeb3rTAWl7waDPLPOIhS5C XHVfOm7cPmRn3pT2VuR8y74U7a1uOkYMgJnCWb8lSXhbqC89FatLnAhKqo4I9oD8 2BXgYeIpP5/OWBcjzmsMnowrvokc0chAmAR0Ux6AP0eX9amC0lGMuTHdw3+is0AR lhoImOUPXvgMH7W2RimpSgnX0R5wKqfuGwMfbGa0xhWBZ+wekAKcU8b+pIHDyX0c EQcir2y8/lVjECXSAIlV6iasPQ3hm1sd0xq1hx4yrwYFvQb7yEhOXbK24HLr/20D RRmEOuS8gg2XtUFv66z/VOw/nUleIg9GAuWDJaiu9frmIma4/tIY4qY= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/malevolent-certs/localregistry.cert000066400000000000000000000021431313450123100317130ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfugAwIBAgIQZRKt7OeG+TlC2riszYwQQTALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDgyMDIz MjE0OVoXDTE4MDgwNDIzMjE0OVowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV BAMTDWxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQDPdsUBStNMz4coXfQVIJIafG85VkngM4fV7hrg7AbiGLCWvq8cWOrYM50G9Wmo twK1WeQ6bigYOjINgSfTxcy3adciVZIIJyXqboz6n2V0yRPWpakof939bvuAurAP tSqQ2V5fGN0ZZn4J4IbXMSovKwo7sG3X6i4q/8DYHZ/mKjvCRMPC3MGWqunknpkm dzyKbIFHaDKlAqIOwTsDhHvGzm/9n3D+h4sl5ZPBobuBEV2u5GR0H5ujak4+Kczt thCWtRkzCfnjW0TEanheSYJGu8OgCGoFjQnHotgqvOO6iHZCsrB3gf8WQeou+y9e +OyLZv3FmqdC9SXr3b0LGQTFAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIAoDAMBgNV HRMBAf8EAjAAMBgGA1UdEQQRMA+CDWxvY2FscmVnaXN0cnkwCwYJKoZIhvcNAQEL A4IBAQC/PP2Y9QVhO8t4BXML1QpNRWqXG8Gg0P1XIh6M6FoxcGIodLdbzui828YB wm9ZlyKars+nDdgLdQWawdV7hSd6s2NeQlHYQSGLsdTAVkgIxiD7D2Tw3kAZ6Zrj dPikoVAc+rBMm/BXQLzy95IAbBVOHOpBkOOgF+TYxeLnOc3GzbUqBi1Pq97DMaxr DaDuywH55P/6v7qt610UIsZ6+RZ78iiRx4Q+oRxEqGT0rXI76gVxOFabbJuFr1n1 kEWa3u/BssJzX3KVAm7oUtaBnj2SH5fokFmvZ5lBXA4QO/5doOa8yZiFFvvQs7EY SWDxLrvS33UCtsCcpPggjehnxKaC -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/malevolent-certs/localregistry.key000066400000000000000000000032171313450123100315510ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAz3bFAUrTTM+HKF30FSCSGnxvOVZJ4DOH1e4a4OwG4hiwlr6v HFjq2DOdBvVpqLcCtVnkOm4oGDoyDYEn08XMt2nXIlWSCCcl6m6M+p9ldMkT1qWp KH/d/W77gLqwD7UqkNleXxjdGWZ+CeCG1zEqLysKO7Bt1+ouKv/A2B2f5io7wkTD wtzBlqrp5J6ZJnc8imyBR2gypQKiDsE7A4R7xs5v/Z9w/oeLJeWTwaG7gRFdruRk dB+bo2pOPinM7bYQlrUZMwn541tExGp4XkmCRrvDoAhqBY0Jx6LYKrzjuoh2QrKw d4H/FkHqLvsvXvjsi2b9xZqnQvUl6929CxkExQIDAQABAoIBAQCZjCUI7NFwwxQc m1UAogeglMJZJHUu+9SoUD8Sg34grvdbyqueBm1iMOkiclaOKU1W3b4eRNNmAwRy nEnW4km+4hX48m5PnHHijYnIIFsd0YjeT+Pf9qtdXFvGjeWq6oIjjM3dAnD50LKu KsCB2oCHQoqjXNQfftJGvt2C1oI2/WvdOR4prnGXElVfASswX4PkP5LCfLhIx+Fr 7ErfaRIKigLSaAWLKaw3IlL12Q/KkuGcnzYIzIRwY4VJ64ENN6M3+KknfGovQItL sCxceSe61THDP9AAI3Mequm8z3H0CImOWhJCge5l7ttLLMXZXqGxDCVx+3zvqlCa X0cgGSVBAoGBAOvTN3oJJx1vnh1mRj8+hqzFq1bjm4T/Wp314QWLeo++43II4uMM 5hxUlO5ViY1sKxQrGwK+9c9ddxAvm5OAFFkzgW9EhDCu0tXUb2/vAJQ93SgqbcRu coXWJpk0eNW/ouk2s1X8dzs+sCs3a4H64fEEj8yhwoyovjfucspsn7t1AoGBAOE2 ayLKx7CcWCiD/VGNvP7714MDst2isyq8reg8LEMmAaXR2IWWj5eGwKrImTQCsrjW P37aBp1lcWuuYRKl/WEGBy6JLNdATyUoYc1Yo+8YdenekkOtOHHJerlK3OKi3ZVp q4HJY9wzKg/wYLcbTmjjzKj+OBIZWwig73XUHwoRAoGBAJnuIrYbp1aFdvXFvnCl xY6c8DwlEWx8qY+V4S2XX4bYmOnkdwSxdLplU1lGqCSRyIS/pj/imdyjK4Z7LNfY sG+RORmB5a9JTgGZSqwLm5snzmXbXA7t8P7/S+6Q25baIeKMe/7SbplTT/bFk/0h 371MtvhhVfYuZwtnL7KFuLXJAoGBAMQ3UHKYsBC8tsZd8Pf8AL07mFHKiC04Etfa Wb5rpri+RVM+mGITgnmnavehHHHHJAWMjPetZ3P8rSv/Ww4PVsoQoXM3Cr1jh1E9 dLCfWPz4l8syIscaBYKF4wnLItXGxj3mOgoy93EjlrMaYHlILjGOv4JBM4L5WmoT JW7IaF6xAoGAZ4K8MwU/cAah8VinMmLGxvWWuBSgTTebuY5zN603MvFLKv5necuc BZfTTxD+gOnxRT6QAh++tOsbBmsgR9HmTSlQSSgw1L7cwGyXzLCDYw+5K/03KXSU DaFdgtfcDDJO8WtjOgjyTRzEAOsqFta1ige4pIu5fTilNVMQlhts5Iw= -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/malevolent.bats000066400000000000000000000104221313450123100257050ustar00rootroot00000000000000#!/usr/bin/env bats # This tests various expected error scenarios when pulling bad content load helpers host="localregistry:6666" base="malevolent-test" function setup() { tempImage $base:latest } @test "Test malevolent proxy pass through" { docker_t tag -f $base:latest $host/$base/nochange:latest run docker_t push $host/$base/nochange:latest echo $output [ "$status" -eq 0 ] has_digest "$output" run docker_t pull $host/$base/nochange:latest echo "$output" [ "$status" -eq 0 ] } @test "Test malevolent image name change" { imagename="$host/$base/rename" image="$imagename:lastest" docker_t tag -f $base:latest $image run docker_t push $image [ "$status" -eq 0 ] has_digest "$output" # Pull attempt should fail to verify manifest digest run docker_t pull "$imagename@$digest" echo "$output" [ "$status" -ne 0 ] } @test "Test malevolent altered layer" { image="$host/$base/addfile:latest" tempImage $image run docker_t push $image echo "$output" [ "$status" -eq 0 ] has_digest "$output" # Remove image to ensure layer is pulled and digest verified docker_t rmi -f $image run docker_t pull $image echo "$output" [ "$status" -ne 0 ] } @test "Test malevolent altered layer (by digest)" { imagename="$host/$base/addfile" image="$imagename:latest" tempImage $image run docker_t push $image echo "$output" [ "$status" -eq 0 ] has_digest "$output" # Remove image to ensure layer is pulled and digest verified docker_t rmi -f $image run docker_t pull "$imagename@$digest" echo "$output" [ "$status" -ne 0 ] } @test "Test malevolent poisoned images" { truncid="777cf9284131" poison="${truncid}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa32" image1="$host/$base/image1/poison:$poison" tempImage $image1 run docker_t push $image1 echo "$output" [ "$status" -eq 0 ] has_digest "$output" image2="$host/$base/image2/poison:$poison" tempImage $image2 run docker_t push $image2 echo "$output" [ "$status" -eq 0 ] has_digest "$output" # Remove image to ensure layer is pulled and digest verified docker_t rmi -f $image1 docker_t rmi -f $image2 run docker_t pull $image1 echo "$output" [ "$status" -eq 0 ] run docker_t pull $image2 echo "$output" [ "$status" -eq 0 ] # Test if there are multiple images run docker_t images echo "$output" [ "$status" -eq 0 ] # Test images have same ID and not the poison id1=$(docker_t inspect --format="{{.Id}}" $image1) id2=$(docker_t inspect --format="{{.Id}}" $image2) # Remove old images docker_t rmi -f $image1 docker_t rmi -f $image2 [ "$id1" != "$id2" ] [ "$id1" != "$truncid" ] [ "$id2" != "$truncid" ] } @test "Test malevolent altered identical images" { truncid1="777cf9284131" poison1="${truncid1}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa32" truncid2="888cf9284131" poison2="${truncid2}d77ca0863fb7f054c0a276d7e227b5e9a5d62b497979a481fa64" image1="$host/$base/image1/alteredid:$poison1" tempImage $image1 run docker_t push $image1 echo "$output" [ "$status" -eq 0 ] has_digest "$output" image2="$host/$base/image2/alteredid:$poison2" docker_t tag -f $image1 $image2 run docker_t push $image2 echo "$output" [ "$status" -eq 0 ] has_digest "$output" # Remove image to ensure layer is pulled and digest verified docker_t rmi -f $image1 docker_t rmi -f $image2 run docker_t pull $image1 echo "$output" [ "$status" -eq 0 ] run docker_t pull $image2 echo "$output" [ "$status" -eq 0 ] # Test if there are multiple images run docker_t images echo "$output" [ "$status" -eq 0 ] # Test images have same ID and not the poison id1=$(docker_t inspect --format="{{.Id}}" $image1) id2=$(docker_t inspect --format="{{.Id}}" $image2) # Remove old images docker_t rmi -f $image1 docker_t rmi -f $image2 [ "$id1" == "$id2" ] [ "$id1" != "$truncid1" ] [ "$id2" != "$truncid2" ] } @test "Test malevolent resumeable pull" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" version_check registry "$GOLEM_DISTRIBUTION_VERSION" "2.3.0" imagename="$host/$base/resumeable" image="$imagename:latest" tempImage $image run docker_t push $image echo "$output" [ "$status" -eq 0 ] has_digest "$output" # Remove image to ensure layer is pulled and digest verified docker_t rmi -f $image run docker_t pull "$imagename@$digest" echo "$output" [ "$status" -eq 0 ] } docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/000077500000000000000000000000001313450123100240105ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/Dockerfile000066400000000000000000000005641313450123100260070ustar00rootroot00000000000000FROM nginx:1.9 COPY nginx.conf /etc/nginx/nginx.conf COPY registry.conf /etc/nginx/conf.d/registry.conf COPY docker-registry-v2.conf /etc/nginx/docker-registry-v2.conf COPY registry-noauth.conf /etc/nginx/registry-noauth.conf COPY registry-basic.conf /etc/nginx/registry-basic.conf COPY test.passwd /etc/nginx/test.passwd COPY ssl /etc/nginx/ssl COPY v1 /var/www/html/v1 docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/docker-registry-v2.conf000066400000000000000000000005701313450123100303230ustar00rootroot00000000000000proxy_pass http://docker-registry-v2; proxy_set_header Host $http_host; # required for docker client's sake proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 900; docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/nginx.conf000066400000000000000000000022551313450123100260060ustar00rootroot00000000000000user nginx; worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { worker_connections 1024; } http { include /etc/nginx/mime.types; default_type application/octet-stream; log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; access_log /var/log/nginx/access.log main; sendfile on; keepalive_timeout 65; include /etc/nginx/conf.d/*.conf; } # Setup TCP proxies stream { # Malevolent proxy server { listen 6666; proxy_pass malevolent:6666; } # Registry configured for token server server { listen 5554; listen 5555; proxy_pass registryv2token:5000; } # Token server server { listen 5556; proxy_pass tokenserver:5556; } # Registry configured for token server with oauth server { listen 5557; listen 5558; proxy_pass registryv2tokenoauth:5000; } # Token server with oauth server { listen 5559; proxy_pass tokenserveroauth:5559; } } docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/registry-basic.conf000066400000000000000000000004061313450123100276060ustar00rootroot00000000000000client_max_body_size 0; chunked_transfer_encoding on; location /v2/ { auth_basic "registry.localhost"; auth_basic_user_file test.passwd; add_header 'Docker-Distribution-Api-Version' 'registry/2.0' always; include docker-registry-v2.conf; } docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/registry-noauth.conf000066400000000000000000000001711313450123100300220ustar00rootroot00000000000000client_max_body_size 0; chunked_transfer_encoding on; location /v2/ { include docker-registry-v2.conf; } docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/registry.conf000066400000000000000000000164441313450123100265400ustar00rootroot00000000000000# Docker registry proxy for api version 2 upstream docker-registry-v2 { server registryv2:5000; } # No client auth or TLS server { listen 5000; server_name localhost; # disable any limits to avoid HTTP 413 for large image uploads client_max_body_size 0; # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) chunked_transfer_encoding on; location /v2/ { # Do not allow connections from docker 1.5 and earlier # docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents if ($http_user_agent ~ "^(docker\/1\.(3|4|5(?!\.[0-9]-dev))|Go ).*$" ) { return 404; } include docker-registry-v2.conf; } } # No client auth or TLS (V2 Only) server { listen 5002; server_name localhost; # disable any limits to avoid HTTP 413 for large image uploads client_max_body_size 0; # required to avoid HTTP 411: see Issue #1486 (https://github.com/docker/docker/issues/1486) chunked_transfer_encoding on; location / { include docker-registry-v2.conf; } } # TLS Configuration chart # Username/Password: testuser/passpassword # | ca | client | basic | notes # 5440 | yes | no | no | Tests CA certificate # 5441 | yes | no | yes | Tests basic auth over TLS # 5442 | yes | yes | no | Tests client auth with client CA # 5443 | yes | yes | no | Tests client auth without client CA # 5444 | yes | yes | yes | Tests using basic auth + tls auth # 5445 | no | no | no | Tests insecure using TLS # 5446 | no | no | yes | Tests sending credentials to server with insecure TLS # 5447 | no | yes | no | Tests client auth to insecure # 5448 | yes | no | no | Bad SSL version server { listen 5440; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem; include registry-noauth.conf; } server { listen 5441; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem; include registry-basic.conf; } server { listen 5442; listen 5443; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-noauth.conf; } server { listen 5444; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-basic.conf; } server { listen 5445; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem; include registry-noauth.conf; } server { listen 5446; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem; include registry-basic.conf; } server { listen 5447; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localhost-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-noauth.conf; } server { listen 5448; server_name localhost; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localhost-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localhost-key.pem; ssl_protocols SSLv3; include registry-noauth.conf; } # Add configuration for localregistry server_name # Requires configuring /etc/hosts to use # Set /etc/hosts entry to external IP, not 127.0.0.1 for testing # Docker secure/insecure registry features server { listen 5440; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; include registry-noauth.conf; } server { listen 5441; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; include registry-basic.conf; } server { listen 5442; listen 5443; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-noauth.conf; } server { listen 5444; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-basic.conf; } server { listen 5445; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem; include registry-noauth.conf; } server { listen 5446; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem; include registry-basic.conf; } server { listen 5447; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-noca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-noca+localregistry-key.pem; ssl_client_certificate /etc/nginx/ssl/registry-ca+ca.pem; ssl_verify_client on; include registry-noauth.conf; } server { listen 5448; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; ssl_protocols SSLv3; include registry-noauth.conf; } # V1 search test # Registry configured with token auth and no tls # TLS termination done by nginx, search results # served by nginx upstream docker-registry-v2-oauth { server registryv2tokenoauthnotls:5000; } server { listen 5600; server_name localregistry; ssl on; ssl_certificate /etc/nginx/ssl/registry-ca+localregistry-cert.pem; ssl_certificate_key /etc/nginx/ssl/registry-ca+localregistry-key.pem; root /var/www/html; client_max_body_size 0; chunked_transfer_encoding on; location /v2/ { proxy_buffering off; proxy_pass http://docker-registry-v2-oauth; proxy_set_header Host $http_host; # required for docker client's sake proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 900; } location /v1/search { if ($http_authorization !~ "Bearer [a-zA-Z0-9\._-]+") { return 401; } try_files /v1/search.json =404; add_header Content-Type application/json; } } docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/000077500000000000000000000000001313450123100246115ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+ca.pem000066400000000000000000000033651313450123100301330ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE9TCCAt+gAwIBAgIQMsdPWoLAso/tIOvLk8R/sDALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTQwMVoXDTE4MDUxMDIwNTQwMVowJjERMA8GA1UEChMIUXVpY2tUTFMxETAPBgNV BAMTCFF1aWNrVExTMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1YeX GTvXPKlWA2lMbCvIGB9JYld/otf8aqs6euVJK1f09ngj5b6VoVlI8o1ScVcHKlKx BGfPMThnM7fiEmsfDSPuCIlGmTqR0t4t9dHRnLBGbZmR8JdAs7LKpP+PFYu0JTIT wFcjXIs+45cIF2HpsYY6zkj0bmNsyYmT1U1BTW+qqmhvc0Jkr+ikElOQ93Pn7zIO cXtxdERdzdzXY5cfL3CCaoJDgXOsKPQfYrCi5Zl6sLZVBkIc6Q2fErSIjTp45+NY AjiOxfUT0MOFtA0/HzYvVp3gTNPGEWM3dF1hwzCqJ32odbw/3TiFCEeC1B82p1sR sgoFZ6Vbfy9fMhB5S7BBtbqF09Yq/PMM3drOvWIxMF4aOY55ilrtKVwmnckiB0mE CPOColUUyiWIwwvp82InYsX5ekfS4x1mX1iz8zQEuTF5QHdKiUfd4A33ZMf0Ve6p y9SaMmos99uVQMzWlwj7nVACXjb9Ee6MY/ePRl7Z2gBxEYV41SGFRg8LNkQ//fYk o2vJ4Bp4aOh/O3ZQNv1eqEDmf/Su5lYCzURyQ2srcRRdwpteDPX+NHYn2d07knHN NQvOJn6EkcsDbgp0vSr6mFDv2GZWkTOAd8jZyrcErrLHAxRNm0Va+CEIKLhswf1G Y2kFkPL1otI8OSDvdJSjZ2GjRSwXhM2Mf3PzfAkCAwEAAaMjMCEwDgYDVR0PAQH/ BAQDAgCkMA8GA1UdEwEB/wQFMAMBAf8wCwYJKoZIhvcNAQELA4ICAQDBxOHKnF9z PZWPNKDRmBPtmnU2IHh6JJ9HzqGALJJbBU0MUSD/aLBBkYeS0YSHgYZ1hXLsfuRU lm/czV41hU1FTDqS2fFpcAAGH+6/rwyfrz+GYr2K4b/ijCwOMbMrDWO54zqZT3KU GFBpkrh4fNyKdgUNJsy0Q0it3gOGSUmLvEQUzqxPFVz7h/pF/Cecr0/kpjbpsxna XQkhtDyKDIQfPCq8Ci1vox5WvBbBkdzDtyCm+KSb6VC3pCX6LV5NkS7YM7mtscTi QdYfLbKX05kUVG2R9SShJn5BSXzGk9M5FR5koGY0lMHwmJqaOqazXjqa1jR7UNDK UyExHIXSqJ+nCf4bChEsaC1uwu3Gr7PfP41Zb2U3Raf8UmFnbz6Hx0sS4zBvyJ5w Ntemve4M1mB7++oLZ4PkuwK82SkQ8YK0z+lGJQRjg/HP3fVETV8TlIPJAvg7bRnH sMrLb/V+K6iY+08kQ2rpU02itRjKnU/DLoha4KVjafY8eIcIR2lpwrYjx+KYpkcF AMEC7MnuzhyUfDL++GO6XGwRnx2E54MnKtkrECObMSzwuLysPmjhrEUH6YR7zGib KmN6vQkA4s5053R+Tu0k1JGaw90SfvcW4bxGcFjU4Kg0KqlY1y8tnt+ZiHmK0naA KauB3KY1NiL+Ng5DCzNdkwDkWH78ZguI2w== -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+client-cert.pem000066400000000000000000000033651313450123100317610ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE9TCCAt+gAwIBAgIRAKbgxG1zgQI81ISaHxqLfpcwCwYJKoZIhvcNAQELMCYx ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNTA1MjYy MDU0MjJaFw0xODA1MTAyMDU0MjJaMBMxETAPBgNVBAoTCFF1aWNrVExTMIICIjAN BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq0Pc8DQ9AyvokFzm9v4a+29TCA3/ oARHbx59G+GOeGkrwG6ZWSZa/oNEJf3NJcU00V04k+fQuVoYBCgBXec9TEBvXa8M WpLxp5U9LyYkv0AiSPfT2fJEE8mC+isMl+DbmgBcShwRXpeZQyIbEJhedS8mIjW/ MgJbdTylEq1UcZSLMuky+RWv10dw02fLuN1302OgfJRZooPug9rPYHHGbTB0o7II hGlhziLVTKV9W1RP8Aop8TamSD85OV6shDaCvmMFr1YNDjcJJ5MGMaSmq0Krq9v4 nFwmuhOo8gvw/HhzYcxyMHnqMt6EgvbVWwXOoW7xiI3BEDFV33xgTp61bFpcdCai gwUNzfe4/dHeCk/r3pteWOxH1bvcxUlmUB65wjRAwKuIX8Z0hC4ZlM30o+z11Aru 5QqKMrbSlOcd6yHT6NM1ZRyD+nbFORqB8W51g344eYl0zqQjxTQ0TNjJWDR2RWB/ Vlp5N+WRjDpsBscR8kt2Q1My17gWzvHfijGETZpbvmo2f+Keqc9fcfzkIe/VZFoO nhRqhl2PSphcWdimk8Bwf5jC2uDAXWCdvVWvRSP4Xg8zpDwLhlsfLaWVH9n+WG3j NLQ8EmHWaZlJSeW4BiDYsXmpTAkeLmwoS+pk2WL0TSQ7+S3DyrmTeVANHipNQZeB twZJXIXR6Jc8hgsCAwEAAaM1MDMwDgYDVR0PAQH/BAQDAgCgMBMGA1UdJQQMMAoG CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwCwYJKoZIhvcNAQELA4ICAQCl0cTLbLIn XFuxreei+y6TlG2Z5XcxJ84mr8VLAaQMlJOLZV0O/suFBu9KqBuvPaHhGRnKE2uw Vxdj9qaDdvmvuzi4jYyUA/sQuqq1+wHwGTadOi9r0IsL8OxzsG16OlhuXzhoQVdw C9z1jad4HC7uihQ5yhl2ltAA+h5G0Sr1b9El2mx4p6BV+okmTvrqrmjshQb1GZwx jG6SJ/uvjGf7rn09ZyYafF9ZDTMNodNXjW8orqGlFdXZLPFJ9agUFfwWfqD2lrtm Fu+Ei0ZvKOtyzmh06eO2aGAHJCBTfcDM4tBKBKp0MOMoZkcQQDNpSyI12j6s1wtx /1dC8QDyfFpZFXTbKn3q+6MpR+u5zqVquYjwP5DqGTvX0e1sLSthv7LRiOi0qHv1 bZ8JoWhRMNumui9mzwar5t20ExcWxGxizZY+t+OIj4kaAeRoKK6r6FrYBnTjM+iR +xtML5UHPOSmYfNcai0Wn4T7hwpgnCJ+K7qGYjFUCarsINppQEwkxHAvuX+asc38 nA0wd7ByulkMJph0gP6j6LuJf28JODi6EQ7FcQItMeTuPrc+mpqJ4jP7vTTSJG7Q wvqXLMgFQFR+2PG0s10hbY/Y/nwZAROfAs7ADED+EcDPTl/+XjVyo/aYIeOb/07W SpS/cacZYUsSLgB4cWbxElcc/p7CW1PbOA== -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+client-key.pem000066400000000000000000000062531313450123100316130ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEAq0Pc8DQ9AyvokFzm9v4a+29TCA3/oARHbx59G+GOeGkrwG6Z WSZa/oNEJf3NJcU00V04k+fQuVoYBCgBXec9TEBvXa8MWpLxp5U9LyYkv0AiSPfT 2fJEE8mC+isMl+DbmgBcShwRXpeZQyIbEJhedS8mIjW/MgJbdTylEq1UcZSLMuky +RWv10dw02fLuN1302OgfJRZooPug9rPYHHGbTB0o7IIhGlhziLVTKV9W1RP8Aop 8TamSD85OV6shDaCvmMFr1YNDjcJJ5MGMaSmq0Krq9v4nFwmuhOo8gvw/HhzYcxy MHnqMt6EgvbVWwXOoW7xiI3BEDFV33xgTp61bFpcdCaigwUNzfe4/dHeCk/r3pte WOxH1bvcxUlmUB65wjRAwKuIX8Z0hC4ZlM30o+z11Aru5QqKMrbSlOcd6yHT6NM1 ZRyD+nbFORqB8W51g344eYl0zqQjxTQ0TNjJWDR2RWB/Vlp5N+WRjDpsBscR8kt2 Q1My17gWzvHfijGETZpbvmo2f+Keqc9fcfzkIe/VZFoOnhRqhl2PSphcWdimk8Bw f5jC2uDAXWCdvVWvRSP4Xg8zpDwLhlsfLaWVH9n+WG3jNLQ8EmHWaZlJSeW4BiDY sXmpTAkeLmwoS+pk2WL0TSQ7+S3DyrmTeVANHipNQZeBtwZJXIXR6Jc8hgsCAwEA AQKCAgBJcL1iR5ROMtr0ZNIp4gciALfjQVV3gb48GR/e/9b/LWI0j3i0sOzeLN3h SLda1fjzOn1Td1ma0dZwmdMUOF+hvhPDYZfzkwWLLkThXgLt/At3rMYstGWa8pN2 wVUSH7sri7IHmYedP3baQdrHP/9pUsGQc+m8ASTE3i+PFcKbPe5+818HTtRrhVgN X3oNmPKUNCmSom7ZcKer5P1+Ruum0NuDgomCdkoZgfhjeKeLrVjl/wXDSQL/AhWA 02c4/sML7xx19nl8uf7z+Gj0ir1pvRouhRJTwnRc4KdWu+Yn7WLU8j2ZKf5St/as zjnpYVEdCp0KSHccgXtobUZDEG2NCHmM6gR2j3qgoUAYjHyqPYlph2r5C47q+p4c dDWkpwZwGiuYq9qpZj24X6BfppxExcX6AwOgFLZLp80IynwrMVxFsDd2J+KpKRQ1 +ZtYPcULwInF9MNi/dv84pxGOmmOaIUyjN8Sw4eqANU4T5uvTjUj7Ou6KYyfmxgG y++vjpRN7tN1t1Hwde8SVWobvmhU+5SJVHV8INoJD7uciaevPo9pt833SQTtDXeY PVBhOKO7thAxdUiqlU/1nGTXnf1VO6wAjaVYoTnP4tJ97WuTptwd2F5znVWHFGVh lzJAzmFOuyCnRnInsf4n5EmWJnT7XF2CofQqAJ8NIddrU8GnQQKCAQEAyqWAiPMK I/dMzlS7oJGlhbKZ5R4buc+EoZqtW7/8/S+0L6IaQvpEUilD+aDQyaxXjoKiQQL+ 0UeeSmF/zU5BsOTpB8AuJUfYoUe0N+x7hO5eIcoCB/QWYX+iC3tCN4j1Iwt6VliV PBYEiLUYPngSIHob/nK8UtgxrWQ3Fik9XJtWhePHrvMvDBalgCKdnyhuucGxKUjc TtPcyMFdi0z4Kt/FAm+5u/v4ZkO909Ish0FrAqQ9t5ETfvTTTYKBmzny6/LSPTK9 0XIsHltuC1xG4vGQsES/Ph++Yj3Vn011FqvFZeBUHbfcQuB4h5wcb+90d4GU1kux eabsHPIZKrlN4QKCAQEA2Fs8NAN5K9i7qbxZCJPi6DJV6XMznk6JVGb+qkkChCyq IOXb95+c9CIpe6w2d3res3zvML3zbdz2Lyp9G0ve6tSlOaSnHeyIxZ5SRB+yQrcF GXtsx370bOGjCi1/NH85kwKlMuROFJKleJQv8rKpIEo5aPSPV9Cc/VsUqBpvR+O0 U1HMv57P4yJA/ddw6imHJBl3jTmWBpK4B+LBsCbdypxdVoO8t32Lb2BqDTaPJfYU RJUpjn/efLLoP6CWxYtqpUlY5tc7NJGAokl8Fo1mPn02klydvs09uiXE80Li2Hoc /meMH07Lbt2VTw6iGNRX6VpIHEUZGZeS6rbAvO4ZawKCAQEAjOtGVPXdyWEB0kHu MBzYY/7tMf0b/rymWNL9Vt5NiauQu8cYSBdNR21WzdLdHkFwqbOCLX9twA7zrnna q+SNnfuxaShlbptls9HvKyySQMCaSRj3DJzaq3ZcM2vFgmUFQxeKPV1geeY9xOta LqbExDzmFq2m9F1PPmqAPDL1bt6+7mCVzb1irB9be52WysUNKrPdBP6b5V1DHYAK EwK1WOs/TxBusqDn/gWBjjmLqYr+ZVndaTfDvPd3sWDdzBoiKZ40QUZ15Z5lu76M 6e2DhfHCUjGcZBEjDaI+WYc9s0REAzJajEf9Lax3ZKZUyCpWbXx5CgSdKCHB8+cP RTyTQQKCAQEAsxx8r5a8hocLfQ43Kvm7HH0nUHeVoRXlbOFDLNf6ZE/RnCCOxOX3 esiZTRAZmzo2CaOBJPnr/+SwTgW/woxCBGh8TEc6LnS2GdviwRD4c3CuoRTjzhgU 49q8Ld3SdDRrBoBnIMWOuktY/4S2WRZ9GwU3l+L2lD1Y6gmwBSa1P2+Lxnpupagk 9CVUZpEnokM05LbMmTa2M8Tc43Je5KSYcnaWctvmrIUbnN3VjhC/2y5oQwq1d4n2 N4eo65vXlbzAUgtxtNEz62YVdsSdHNJ8dXkVZ3+S+/VPh75i2PxjbdFSFW7Futlx YtvAEs3LdgC8squSDQ1LJTutXfBjiUUX9wKCAQBiCMre86tLyJu6Qb6X1cRAwO7m 4kyGzIUtijXko6mWxb4X/usVvzhSaNVYbHbMZXjX+J5vhBOul+RmQ3EY6nw0H2z8 9D4z/rnQVqeb0uvIeUhBPni+s4fS4bA92M6Ie5bhiOSF2JjjJr38BFnTZARE7C+7 ZII7z2c0eQz/wAAt9fWWroAB2mIm6wxq0LNij2NoE0iq6k2xJE1/k8qhXpsN0zAv bjG72Q7WryBeK/eIDK9e5wGlfLVDOx2Evlcaj70oJxuoRh57e8fCYy8huJQT+Wlx Qw4zhxiyzAMq8SEqFsm8dVO4Bu2FwzmmehA80ieSb+si7JZU92xGDT394Im2 -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+localhost-cert.pem000066400000000000000000000034151313450123100324670ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFCTCCAvOgAwIBAgIQdcXDOHrLsd2ENSfj5h8ZmjALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTQwM1oXDTE4MDUxMDIwNTQwM1owJzERMA8GA1UEChMIUXVpY2tUTFMxEjAQBgNV BAMTCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2K saEVcHq0eldu5kABbWtZsf9keK7lz8beVIowzOqp5IHpGlggtH7xDVeigA/sLdds WTgKEOq3zsJzdgfEti5TNAjjmPqjMKkolqv3LXDJG0dZ2GZ8W/eBB6X1wB0LKr3i ye3/5jb/wCZYVGGMQXj0VQxY8Qq+OHEp0effeheJqA0OYOj+RaZwi20OR/KmJRgY wXU33bZyapuyT4krhFlFbtzXeKsKQPrT2ePWxPAceqUGUTIqyJySYIw6vb72YxjX FNRw6Jg7B7RqVJaVCfBrVxtAv+rCLOhUOVYmWhgWEIODPXiqOGwB0VUApAVAYqfi TYnJIZ7QYLlQx5VPNlzZuSJTUzKmHQLtLcTqdO5HmLxfxc0WuS/ftK916wy/jpSc m2DiHjIy6aAEaHKGQrNgT+no68kp30xkYAVsIs0BFpl6Q2iNr5e0uKta82A0xU1Q we7swSHOHCevuDZfFA/CqnBptOjvNUuVytcroCeCrV/ftp75w/Fd9zOcb6LGLxM2 2UzhkSXl3II250xj74Q3q8T9TDxCLty7oiawhaYKI+8SDYc510EQ7MH46WMO+3Uq JkpmmELd9POgnnZ1JrCFmf0flUKTi2CqU3wrBPpPMwFBxoFipp5iL87npACHc3DY 6uaoF4Pf9Et1Fd7HRon8RMsKkrSF92NFiBx5UvhZAgMBAAGjNjA0MA4GA1UdDwEB /wQEAwIAoDAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDALBgkq hkiG9w0BAQsDggIBAC0F4ci1nqZ9KUhEEAmWmy8g89DovNNIGSC51r2WJ/COmYUX X70TONscsBL/kx5MK4xoAmb+EN6Yy8i+z9NkNJd0B+2MjXPMFBpgGb0UiPv2wEmZ 5PAKyjwTxNIm6L/nFhkmVqfsQHfjHukXES4C0ff6fj6fuDpBfl5nTlVmc9LpP+hT 5RAwW10qumucGxAWGNBWW+K66cf8O7n/0nQykxJxYjBx16ZB80H2uvqFDKDVFqze co5M4euXQq9KiXPRlcC9rab2a7FGLHd0TyPkq6TvfsqpxcryyKS4rIAz3sQh/tl/ /qm1tBcZW2bce3UlF2Wb2dW9HqvIu1O84f6ptLqwgKcIdTbwgQZ0kbFoWE2kWJSV w+eAFb7tz1LDTpF3NRlz+1K27pBQWRQgcqoIRoQXpC0LfQY9Mp70QIfUQdUh6tnO 8hmq5y623tfxiDwCxb/EOpwCmwK1Cp9cloZTDefVE1r6NkEJWeeHG79VljUGF1KT NKzXWrrsFtge/hU9Pj+frcZO9qExxPCcsrdZcoK7Ll8s+pjulRvbnCnJkNpeOI3P iz6+sdGmzKSKg2daRM67Zmy5tmlBEX/eV7kFqt+b3HsdUiLo3Ng2lyPLNNDfwUtB EukgYGjVJoyqLjLXgsCxLJlk7X/ogVwf8SlAnQ7p6KuxGWm02vlUpEmJp+Hq -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+localhost-key.pem000066400000000000000000000062531313450123100323250ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJJwIBAAKCAgEArYqxoRVwerR6V27mQAFta1mx/2R4ruXPxt5UijDM6qnkgeka WCC0fvENV6KAD+wt12xZOAoQ6rfOwnN2B8S2LlM0COOY+qMwqSiWq/ctcMkbR1nY Znxb94EHpfXAHQsqveLJ7f/mNv/AJlhUYYxBePRVDFjxCr44cSnR5996F4moDQ5g 6P5FpnCLbQ5H8qYlGBjBdTfdtnJqm7JPiSuEWUVu3Nd4qwpA+tPZ49bE8Bx6pQZR MirInJJgjDq9vvZjGNcU1HDomDsHtGpUlpUJ8GtXG0C/6sIs6FQ5ViZaGBYQg4M9 eKo4bAHRVQCkBUBip+JNickhntBguVDHlU82XNm5IlNTMqYdAu0txOp07keYvF/F zRa5L9+0r3XrDL+OlJybYOIeMjLpoARocoZCs2BP6ejrySnfTGRgBWwizQEWmXpD aI2vl7S4q1rzYDTFTVDB7uzBIc4cJ6+4Nl8UD8KqcGm06O81S5XK1yugJ4KtX9+2 nvnD8V33M5xvosYvEzbZTOGRJeXcgjbnTGPvhDerxP1MPEIu3LuiJrCFpgoj7xIN hznXQRDswfjpYw77dSomSmaYQt3086CednUmsIWZ/R+VQpOLYKpTfCsE+k8zAUHG gWKmnmIvzuekAIdzcNjq5qgXg9/0S3UV3sdGifxEywqStIX3Y0WIHHlS+FkCAwEA AQKCAgAtZw3V8P/+el1PpqoCsNzpqwvQn36bc3CKvPwtM1tJQa2Q92V3DQdr9rDg 7pjGkankpGorKScH4ZLseLy2h5aKRCZm9PS/DhbbCs1wrDhtO5AxeKYPGhYNiOpx VvwuHQ/Pohfmdn7KgNrKrW1WIBW5CWN+2X4mq2Gk6aYLHgKZSeB3mf1st6mNRACW RZg5OZKW3VMv0a/l3cVaeqooXwQ/PtUkXhMp3ILnnKly3Gulzi2gIyj3EQ5vODSe O3gND/UZOJwwgGG6Aief4fnDc7an+c1OSgBr8OVC21Ys3dfQWWV0os9gVFhymX8k 2AgRf6jP93sFw2NSY34KvcGZpKG59oMDxWF1vPo8sOt17Ey0+qp3eUtB3FfE7Wtf BaLaD/x4U91izIqOEMzQ6QiZAyvmUoBkUSo125CYuIkt8C8Q1lA1KjihETWF37QR mr8LUk0A0x3SErtm4wVfeDEqVSfI9gKpk6i6rlUzuCjv58Rc0yyqoghXwBWM4CKj 5ZHYpBKAxj4bM6IrKnodAOcsyVk2c2zVTaMxPhoUj0fF7IE5Hy6YAQ/yBheZEM1v fhsdBFyS6OqSCnN6UinhH268QPam82lfKTFjW5lOgsSDQZ9rhiWoyamhonJTq65I nb08f4mzT6OGMwV13zq8dXio6WnUIQAhXdEYWrMBmxp5b6CxAQKCAQEA4kmwV3Nb n3ZIzVAp2l+yGZwdg4YWzN2kcfdNkL8I+Pn8pWrOwv/uGQYmM0786ys9kB5lu4FR TMcoEo3AaK/z8N49ro2Kl6HcTmxZgTMr+cl6iwetzqYdkRK7klxyCv5uVloDQDtc AulDH6RkW9BfRERpi6XtlgiFdJj5jMvXMpwGHX69JVsXb83ZSQESjI2JfO9Y8+4M a7hNKWW/W0ZBrGCcQQPbgpysfJ+PFKUF/yF1h8SSCdetW2Kv2ix16wL5uHKINYmZ Y/Om+/AFnUOQlANycgThtgBI5mvg9Khq6W2i/RNcIL7bvwAzq1p+o6cGnImXo4bY hC4fs2/aeX17UQKCAQEAxFQHSLBYDLal5CQYbHbNZ2sLjwRUraEd/+BA8XoERVVQ JPihgEvTPEaHnWrFTw0qaGKgMZ5SZCZSWUIfXjYvQIUcEMhNUOHweXhJJhifO5sd sTuvU7bWg76F69bRKfp8KM266m7qMYv+tNlQ6Kbz/1ImsW00xb86vCK2hPfhldtN d/iBb4HVDu1uoATHUNuqsSGj/UvttKudQdg7MapzM4N+D4m6rPZUjQmtoMWOXt7R LYrqEOHWfkxXKlVHw1cL9uzUpArvnR0VcYvGfXiYJFbXWsEB07VxIoLMPEtPbpH9 YLY37KugrthEVnsbySmZIWCRDEqQuuAaa5o8S1naiQKCAQAiU/dybMebe0A0FVMk E5xbEjnP+AmBbqZBu7iCmthrnNDc70UKg/TEyxAEfJkVu+uM72+TcFy6/wNvPR3R Q9AH3E8TKdm6gw1+wCUb2n1zWUND0Bhn3v9hQKw/2dJbJJnsc59GoTqmHmjWZgPr gcLSAmbYjoVqW0STmZlR6KJuxQiQdOeQwS7fASVTU9xSgi43S7/80UIFHWJnQ04y NIhF9CoAGuuz9ryb80CraxVrzNGdlQ5qe9OKp3/x4wjIbB0iBA3xwTwJ066jTZgs cVF/gr5b2a28BHMKsZbgxqPhYYZ2SfeR6CJB6W/tML9BaFcybBUa85vpAW5BtFg6 UfThAoIBAAp1/71byBVFVimF0tdUrTUpewAv1uM5hoOvy0YSnk+jcBXIObLAV40K pQc6PTEtHmlZd/es2+8CK7kd0NYQRQxHC2vJgHUi1NFkG2GwRivC5B4hdAId5+g1 KqWaWKLH+f2imKcNKeVh9Dxmp+z9mFquYelqTDmNKvADWX5URuzZNpOB5kOuw098 TzyvhH9GdR3jEP3aIdxSmJp9jwnibyj7hKgHSq8UoQSy01GRtThQ3wxyLm6f2fH4 11wmFyDNbpHFpL7o5kOU3SOjsvvUhSbKiccIKbTCIjkYhxFfYegeV0Xj767opjMq ytlgzeY2FTa2EoR5JKUQc9fv6+6H5yECggEAVVfnywPm8QXn+ByFDdUndZg3uEje DGyvt1M3mIz5geyRZO8ECzgsZVzKgZC8jDB4lPKz3AGgNlUl/vyGHk6CtW6V6ePA EXcmOkkMKJQMdopY2/cE6YlSpBGMCcnfothgL0HXxYoop4xVjb74k7tFcNrIDoRx zp9dSalgxx9aMeaURRbMWf8AhWLZUAjJ/359M1SmcNW619SL3p8Q95Nptvdiltww lWOCkBdgkjW0mel+Mi2+gY8UPmgNBMPrJ1z9b7b7529YCv5Oci8ABn/N202nhjCp LupADooNknOMLDyqwRorEv4g6wRjuPIYTIhI9fO5ranu089x+mmGU2tCBw== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+localregistry-cert.pem000066400000000000000000000034321313450123100333610ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFETCCAvugAwIBAgIQJ+iLgsp9gA0DmROqW+tHFzALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTQxNloXDTE4MDUxMDIwNTQxNlowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV BAMTDWxvY2FscmVnaXN0cnkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC AQDHR/A6uiQ9X/Xh5ivmdjRr5XVr1D7+fU9Qu6ohArqtBuJsLr6t2RBTS9w6PIAf xjQSMSFlrm/CY+hbfBMSgm9NeH23o3kYCgoEPhP/634A45W5xwUFno388U8/NHK7 qwzSP1ezKXfXNvzuo1mZhT08aVdGMOrZUcZZZl8R3RPcIRw9XDSfXKVkMluH6egk 8iLdOxdIdRS58DeSI09FskWe3cIZ5kJmMqnKoIbYSJCVVeYPO0RFlIBi+zpdVyI/ r9LG0r0plRdz/HJevbOitU2y93S1s9NWMNEkOFU1PFJmsF3ZzNqJFCySj00y/Hcs jPULYwIxYdqcv16cTNmd3P6FegvuzLJLjNuGaLJGc1antv+p62P7ZdE3DyprFuxs MJgDL9+NjDaIzoamFf0Uv7K3F7hxrrAHfvm1CMUOyQLg9J6Wl4mLsOy2ZhCbdNFs T6dobAUGvz4Muj9V8V5pR+nFehjmsPENSsTcs5j0e8zTWtvMFISdS+NZAkpiz0s4 PV8DLgk5Rp1ZG2V5OnRPLMOTgK0nngc5GVaxf7OYCrFHbBJ8tL93MXNQptNFeBpV FhjUGqVFcz+6nbFX2NsFLZnghQRs9lej4TTG33NSAYusKqhVwpYFf8CsXCcvYuU6 RlkCYjr3PB+nX1UDa0eUGm0zOabf9O3D1VzHQBpDuzSHQwIDAQABozowODAOBgNV HQ8BAf8EBAMCAKAwDAYDVR0TAQH/BAIwADAYBgNVHREEETAPgg1sb2NhbHJlZ2lz dHJ5MAsGCSqGSIb3DQEBCwOCAgEAaPfAs6saij4FZIPbzAb5M6ZVvfXBg+AfH52t p3tFsnWUJCiOh9ywsc2NcmJdleKDc4/spElFMUarHqcE1ua6EH15O5GEnHWKj8EY PVQFrPvf30UkRGNPl8eC7afZtCNk9MLllIATAzBr5Z1i+psV7MmgBKpbZ4B0TnhR GXNT60QaCJ9RfUuc2z7RHJNo9XTn3Q44X7TFj+P3jHOWzTf8y6Mz6saTy2bugIUy AfRgRgq/bB8hRjrazg55FIlrMv7dr3J0cIuqmaHfsw7Q2ECMCXW8oQXMBzfuIT0n sG4u0oVxdNx4OdHsAubGjjwNDhxJvN5j8+YFqZMu03i8LbyamTwsrZg2C3QrRUq8 SujQEEB+AmO0lpuJ24FsOOYVSYCpLy2ugrKOr2NUqbiBKZs8uBh6RGACfunMZlEw 4BntohiO7oZ5gjvhGZNUEqzMChw7knvVjZ+DkhFk9yE4qIL7VsJSUNI2ZJym/Xeq jr/oT8CpP8/mFZspa6DFciPfhGLQqKcaZZohL7461pOYWY5C2vsJNR2ucBZzTFvD BiN/rMnIGFrxUscCCje6RLmrsZ3Lb7bfhB3W6kwzLRfr/XEygAzx6S2mlOM34kqF HFpKrg9TtLIpYLAKAIfuNbrLaNP1UKh7iLarhDz/qDcvRka/qJTzLD3eLeGXefAP KjJ1S7s= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-ca+localregistry-key.pem000066400000000000000000000062531313450123100332200ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAx0fwOrokPV/14eYr5nY0a+V1a9Q+/n1PULuqIQK6rQbibC6+ rdkQU0vcOjyAH8Y0EjEhZa5vwmPoW3wTEoJvTXh9t6N5GAoKBD4T/+t+AOOVuccF BZ6N/PFPPzRyu6sM0j9Xsyl31zb87qNZmYU9PGlXRjDq2VHGWWZfEd0T3CEcPVw0 n1ylZDJbh+noJPIi3TsXSHUUufA3kiNPRbJFnt3CGeZCZjKpyqCG2EiQlVXmDztE RZSAYvs6XVciP6/SxtK9KZUXc/xyXr2zorVNsvd0tbPTVjDRJDhVNTxSZrBd2cza iRQsko9NMvx3LIz1C2MCMWHanL9enEzZndz+hXoL7syyS4zbhmiyRnNWp7b/qetj +2XRNw8qaxbsbDCYAy/fjYw2iM6GphX9FL+ytxe4ca6wB375tQjFDskC4PSelpeJ i7DstmYQm3TRbE+naGwFBr8+DLo/VfFeaUfpxXoY5rDxDUrE3LOY9HvM01rbzBSE nUvjWQJKYs9LOD1fAy4JOUadWRtleTp0TyzDk4CtJ54HORlWsX+zmAqxR2wSfLS/ dzFzUKbTRXgaVRYY1BqlRXM/up2xV9jbBS2Z4IUEbPZXo+E0xt9zUgGLrCqoVcKW BX/ArFwnL2LlOkZZAmI69zwfp19VA2tHlBptMzmm3/Ttw9Vcx0AaQ7s0h0MCAwEA AQKCAgBd61qd4vKHdn1kzNztzdHg9BDGFA7oU9iYvQlua2HdgDwgLluxhXa7Oyp8 y9y6nOgXls4dpPuJCxsMWsqGU7DvOxVNAh9lI/4ah8NXPv5wntIG73Q/dL2Ic5Yc vLRCHFh7klzb1HRlmsXUFmp4/yGgIil+rDlS2MZ5hdTSj3X3ricoCBfI75oHQfB/ es7s8q1ZxKqxfHSbOUqHdlq7B0zmla8QE8RBdCkvlT5YGsMBjq1RimYfwOBNRgf4 y8MZbt0Q1WtPeLPH9zdTzWYnDfmjmhqINEsq+PDoeCA4aciQGxjwOCrapgZnwF/q 4q+r8HbgufXjnjGw5ERLt7BsRSYynoJiTWQ3p/wZ2VLpjFtxYxoJ5/qpQvbZMgGS Yu3FZNC6cnbOs+JWbdm7Kg93N24cBrGdk/KdEE6lz6uQq07FTSqLtPEQWePzBiuA 1wfP78b2AH6vyJKq36EfMCJK2i7rpwtNz7d9NI5kiLRDB7gesqC94WJ+psEu+ErO w9DbTV3xdOPs4FGGrR41Hbo8emrk6smhb8+VK2odggi8i2CLAkYupMsuobBlX3CL hyJPfWDv1aREJ1w7zWVQlJkvp5zR0oXZXpfFxjpj7Ypbp7BKxmh5+WYj8msFDfaD 8VQ+pqgPpdl6zElEq9m5koHjsHH57fMeJQ59HiWpWFur+kQx4QKCAQEA0Jnvbm7R WypbPDInkIoPDIhyP9Pqv+wMzNfYEnVEG0GhEU/H5aE20a+Dm6u0bsmPm5lCSQsu EvylTSL3yumQZMincNIUXcPYb2Qye/ZzJnMIibCqwMKQqi4HxCXprWhiEoGPum8A fN0bTGgMYfM6JZ/Dh1eGsEvemeW+5tn5xZF4Lfp/vkT8v4FuHDydUF/lIx7F5MMi VteS0hHnR1DuvxHqtysf0wy2l61LFr7mQCMYTNEyFB3ZfXqpxJmFmCqPbr4PQsIm 2rqIDw+13eeoyDpJJkdi+yzHkAYDOdAsur0vOQvK/Zj1QKz9qmC1O6L4BN5yp265 vjSE4Orvo7btEQKCAQEA9I/afLw6lHUJ4FVL0p7dH15JSFjt7nmGHocE7Wf6Yp3G vMp+PdGyoJ2KEQB2unnQZK1gZqUuRQLannjNl7fsIiIhHgHxMBCIiylwSUVnP868 u9/fpJV/cSGze2zF0WAttIgXKNtXG7xMntcY2k+SAe0qjqX494KT0NGnznySt2nU A1YlkXm6u3KCOJrBKfbtiHXFoH39sA+ihuPiV7xcETS2ZrFdAX9M422p4yDHqe/0 dTe18wIxJNiEX4xp/HRE//cuQ5dw/Z/QmNrzgWxHbOmXVR5C90vIJRuYY9xz0tDP LMnifSKfnG16l2gqg7zb8xsxYqSGndXWKPAeiq3/EwKCAQEAhCWQbWgcjmFFzNuE /ubG48yoe9DW/OAft8Dg68iH7bBkxd/BpbG8VZeXiw16T1i29f5f5IAFnxeX7EbD rTLLO1113V3ocwH3YZGa/bbBedETzo4xjc1z8asZVmQiJa1ju4+CKrvZFkDH415i wcZgxqbwKhQDijl1+g52Ii5iMYuXE6GGPVXcu8DVrWOk0N7+/IGpIeOQJG2KYDPh TOdzZ22FQKY8EeoS3gF0+SLUIDtbUIaR7/Z86iXD2HzdCemkVaZnaoYuMRBL0ybD sqDn5nguEObWSII0pgN5Fa3QODhS6xOSc5brfx5X0BBVn0L9VbBJ99GIL3t71jRe vVrL0QKCAQB+jUYZT+ncUqgWruy6g7yW89pmFqagxb/SYjn5g9m8WDq0DPDAmped p4f/fkbx/gEJZ/I/i3BjA7QPVyHERcdqblDGz2h4X8XYhUv2jnR8P0XIznNTHo1B BJh04PeIfgWIqveZC8+KqajYdSQGLDC40Ho6MMahha9p2mPEZRAi2x97zoNIQT6Q qxOZqPMV/RIzkAYBI9E33w9ST/AbSHw35xgQEe23zaEC+wdzYc4QMPxF/9smcdbu YyA0tVtO6PefoNAO5/nvNFjkEED7kwVu5X2K7Urn3w4lrZ7w5e4FhEoAukN6T4Va lAhg+uUtIHiM12B50/tZB4N30bFsP9eDAoIBAHc7ppfpo1aDK3bDr6zTSOU4Mn1l XrfhBJHDy2Wt9WkvWtcCtXr3sDpthaChueV+mGoKvfgWyzUoauO6HDDsRYriqaQB cXclVjyy+3atY32Opz9rnWefQkbgTOQ+oQgOzEFhxNS+11Omc6ZZ9s31N6TZi/Yz rgXzhGrr73DkV6uwiiwkvP8vJxg8AMWKorDIm1myr9wwlK5ogDKSku1DM/y1gvlt 4EA39fqURyqxN9o5Yq+8K1+a/smjGx95M+P8Nke4bMs1+lb7bBXbMaVpC6DLqj8B eleOZ7adY2mS0CBuf0PNkJRNDwF1B5VDmGBJLubUtGLuUUoEyUbv66WfnUw= -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+client-cert.pem000066400000000000000000000033611313450123100323120ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIE9DCCAt6gAwIBAgIQb58oJ+9SvWUCcYWA+L1oiTALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTUwMFoXDTE4MDUxMDIwNTUwMFowEzERMA8GA1UEChMIUXVpY2tUTFMwggIiMA0G CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDDmOL3EhBm4So3agPMmF0z1+/nPlrE xoG7x0HYPk5CP3PF3TNVk3ArBPkMzge0/895a4ZEb9j+LUQEjOZa/ZwuLmSjfJSt 9xTXI1ldp8KasyzQZjC33/bUj7FGxGzgbHyJrGGBoH2W5HdswH4WzhCnGTslyiDo VN4hklJ7gr+Geq3TPf8Eji+1L71MOrUyoNp7BaQBQT/gKxK0nV+ZuSk6eaiu+om7 slp3x4bc21o7eIMmNXggJP6p9fMDctnioKhAPcm+5ADiFYSjivLeUQ85VkMTpmdU yvq6ziK3Ls6erD+S3xLvcHYAaeu84qLd7qdPwkHMTQsDpO4vPMIwL8piMzZV+kwL Bq+5xk5//FwnQH0pSo2Nr4vRn+DITZc3GKyGUJQoOUgAdfGNskTt8GXa4IsHn5iw zr12vGaxb//GDm0RLHnh7NVbD8xxDHIJq+fJNFb7MdXa8v31PYebkWuaPhYt6HQC I/D81zwcJIOGfzNITS2ifM5tvMaUXireo4pLC2v2aSY6RrPq1owlB6jGFwGwZSAF O6rxSqWO1gLfhJLzqcw/NjWnO7nCZEs/iKgAa22K2CtTt3dDMTvSBYKdkRe/FYQC MCa7MFJSaH85pYRzoDN4IuVpvROrtuQmlI47oZzb64uCPoA4A8AN+k8iysqITsgK 1m8ePPXhbu4YlwIDAQABozUwMzAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYI KwYBBQUHAwIwDAYDVR0TAQH/BAIwADALBgkqhkiG9w0BAQsDggIBALSgrCdEQd3I vb/FNkNZkAwdjfBD6j7ZtPBwvjEiiyNTx9hOLBGvbey7kr0HtW0KkLWsdRmCc+3z ev9I5VjDOtpiqrvuAA1wRBaL3UzGyj/eFjPJpvkfJi8zjkIZ2y18QG3yJ6Eqy6dD 0aIQAHl9hkXMOVrf364gf0p7EoOGtSlfQ56yIGDPTFKKiy+Al0S42p17lhI4coz9 zGXE1/SiNeZgdsk4zHDqhzzBp8foZuSL1sGcIXHkG8RtqZ1WvCyIPYRyIjIKZcXd JCEM//EbgDzQ7VE/jm+hIlYfPjM7fmUzsfii+bIrp/0HGEU3HN++LsA6eQOwWPa/ PrxKPP36EVXb72QK8C3lmz6y+CHhuuAm0C1b1qmYVEs4eRE21S8eB2l0KUlfOecf xZ1LWp1agKt6fGqRgcsR3/qO27l8W7hlbFNPeOTgr6NQQkEMRW5OxbnZ58ULXqr3 gWh8Na3D4+3j53035UBBQUMmeeFfWCvtr5n0+6BTAi62Cwwu9QQQBM/2f9/9K+B7 cW0xPYtczm+VwJL6/rDtNN9xPWitxab1dkZp2XcHG3VWtYvE2R2EtEoKvvCLPggx zcafsZfcD1wlvtQF7YjykGJnMa0SB0GBl9SQtvGc8PkP39yXHqXZhIoo3fp4qm9v RfbdpOr8p/Ks34ZqQPukFwpM1s/6aicF -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+client-key.pem000066400000000000000000000062531313450123100321500ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKQIBAAKCAgEAw5ji9xIQZuEqN2oDzJhdM9fv5z5axMaBu8dB2D5OQj9zxd0z VZNwKwT5DM4HtP/PeWuGRG/Y/i1EBIzmWv2cLi5ko3yUrfcU1yNZXafCmrMs0GYw t9/21I+xRsRs4Gx8iaxhgaB9luR3bMB+Fs4Qpxk7Jcog6FTeIZJSe4K/hnqt0z3/ BI4vtS+9TDq1MqDaewWkAUE/4CsStJ1fmbkpOnmorvqJu7Jad8eG3NtaO3iDJjV4 ICT+qfXzA3LZ4qCoQD3JvuQA4hWEo4ry3lEPOVZDE6ZnVMr6us4ity7Onqw/kt8S 73B2AGnrvOKi3e6nT8JBzE0LA6TuLzzCMC/KYjM2VfpMCwavucZOf/xcJ0B9KUqN ja+L0Z/gyE2XNxishlCUKDlIAHXxjbJE7fBl2uCLB5+YsM69drxmsW//xg5tESx5 4ezVWw/McQxyCavnyTRW+zHV2vL99T2Hm5Frmj4WLeh0AiPw/Nc8HCSDhn8zSE0t onzObbzGlF4q3qOKSwtr9mkmOkaz6taMJQeoxhcBsGUgBTuq8UqljtYC34SS86nM PzY1pzu5wmRLP4ioAGttitgrU7d3QzE70gWCnZEXvxWEAjAmuzBSUmh/OaWEc6Az eCLlab0Tq7bkJpSOO6Gc2+uLgj6AOAPADfpPIsrKiE7ICtZvHjz14W7uGJcCAwEA AQKCAgBmIvmxpp8l+cH/ub5OIenZXpMJn4fqZPXtxjjd4HshIN0ln0JlF15lOG2M gDGKFGKUts8gAX/ACocQETtgnDnn65XlwPIqfXFGflD2FNoLyjBGinY6LhtIF9is aXmpHz1Q7tDjzZiHKLor8cBlzCjp+MToEMpqR5bO1Qd5M2cro/gM7Lyz9kN3S3x/ x9BCpbgwsVtYxGfEePmFkwAO159tx4WMCYvOlW2kSm5j+a7+iwmA9D7MGkVZHvNN A7Y/H0F8ekdVBN5pMG9Yrv/vk0ht2lugcS5YGr4eufFq0mhWdv+jhBTxLzqPMMBG m9oMJcj8XyXYtwpfVsqBpCqK2wnEnv4Kf0rZzBU706nI2mjPXx3dL+5qo8uQJKNp mxoS7vmHV5RIJgtdvyzGFHjdfu1leowhV+Jy9jWzMw4wlnmlxsfDECf5RoSf2XGt SMGJb0dbJKae+W4MfNUFsgAWMZk3h3KF8AHHe44OpDbQeoh3JLnkWSG0oS3CR0ch 68TzCy0SZZEZ9IS+I6o5WVpwWfReCQ5NjaKipWcpiJvxg+Dc3GG3QcVXVz2gGrJh g9v0v6eyeOJ32QGvvP7THFBjpWeeHlXT8Yz6hFcPrvErEZ029TEmhg8aLWBGfsR5 F1bazdbqvOSEB9vBAAaddNnEDG9Rl8EmC4WdsnVgYUw1J7gfQQKCAQEA9DKjD9eN CrUl/2YfSm2WaFhYci74XcHDVeAXN2SbOyKbMIqk3aOFQNRAsLRnwPkdiLtuqeDK BafrfLTCORHfFdYKnUzmuekESNLckN9VyLztgqOqNAv3LD6GmSHBaJEnUyniLxOL k0wMEBIsEQw7Fb4blM2REYJ3ZzMFmgpRGnIX8KcxhW9XgSrnqMLO0w6mVxjo7xzd 813nCcNrGhySM/EzKYtTNHy2JZmMH5QFHaIj67KklO7VeEZX5U+TKveBEt4rmHqs Ndqf/djSs8vu1xse82pVRxMXX2mhDLmwjUjPgWYxUL92jTiyJhE7GxpVB/yHgF1J Ecb47MDahoNKkQKCAQEAzQzvCOA77IQpGO117GcMqcjzwEUhTytojFBT+s5mHfzk dYr5TyN86LQ7/GktNoJ5oRvD9UGRSul1OGneivqtWj6mv6/Zvfzacx8NXY4MYFs1 nEr3Gr7orVFIzD2x7nMPG2G6+J6hZ1rhpnZ9Hprf5G41sHIJxHJ9wTYSUAmFh8bv FiJqF90bSq/E5hgjphtX6wZWeZYspzc/5+IrJ/I0nqoxV3rjUy234zlzKJAV10sV 5oVgxLLQsUujkHp/Da+ij2aTv1Za8y3PTJ7MAHYgdpa5l/4U9MnPUEB2REBCI1NN TqxnViwD0xgsvxfb79UzruLJIYOCKvfOumlutXM0pwKCAQBUIMXQhWAP2kyW6mXJ TGvO0vDVlZz3H/Pdt/AHo19fRhLU7E7UFKupo/YNanl8H9au7nO3jrvKqwkT02o+ IwwKB81sV7v9PGu/cvWN64MwPvZMVXojqCOlWH0icGCjV66Glh1YPpGNU1ushbYs wVvxp6b04sUhlSLxqMA7S2aZh8j7nX4QDEXHODLLDyIV0Cw6QViuV/GXEDiyQmK5 gjJUNrp7i4ZExNozpeyCTIpepSde4hKVRJrCbumFFJ8M5GvRRj0asNh3TTRlTbd5 Pb6w2KUXEwECFW+t7UQQkEBkzDrAx6YhvXRoPqoRN0p3keDNeZBtBrZPq47CccZX JRAhAoIBAQCJ/DgnGu54XP9i/PksGrSU1Nvi+SJPKoDyW2QIFTj22SXMS7c1oEYA OrlbRFPeqLK8zfhyZKsnZC8zxVqy37okTqDbwbSfezZt3emamWqOtRJAmNnsr6fY aii4+JNySQ9Td9LgV69549iRso7EN6iPCfMrR7J29izWBlMQdTfchOyDUqleYbZp 7hpsVLY4o5HoYJ10uLBX3oAsxTARc5YhZ5pIqjOr18o1KIXsN/napXaZaAwUkdiK VsI9CZHSXezg30Bxs+UEXEFx6DKT5Oo3o3pFZAAqMlxGPvrXNv7K0tXlKXNos7nn Jg+GkMG6hRiAibCb0umXjKcbHrQXeu1lAoIBAQDcRBsy6cSQXMSu6+PyroH+2DvR 4fuiMfSrUNjv+9K8gtjYLetrZUvRuFT3A/KzDrALKyTFTGJk3YlpTaC5iNKd+QK8 6RBJRYeYV16fpX/2ak/8MgfB2gdW//pE0eFjw+qakcUXmo957m7dUXbOrw1VNAET LVBeVnml+2FUj0sTXGwHKcINPR78PWZ8i1ka9DptnKLBNeA+x+OMkCA88RJJegSk /rgDDV52z4fJHQJh9TZ7zLAXxGgDFYLGPTrdeT+D/owuPXF+SCP4pMtVnwbQgH9G dfQ9bb7G14vAeu/kEkFdGFEreS09BOTRbTfzFjFdDvSV4JyOXe9i/sUDxf9R -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+localhost-cert.pem000066400000000000000000000034151313450123100330240ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFCTCCAvOgAwIBAgIQPjclBRGzhznCybQzYRQTyjALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTQ1NloXDTE4MDUxMDIwNTQ1NlowJzERMA8GA1UEChMIUXVpY2tUTFMxEjAQBgNV BAMTCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALBe C9O6es+mStDowUd1kiM59VkinzzdHgE24LvKmGxQ6fDnnT8S9L7iyzoxcJWlvSHu pfyZWvij0ZIyRZ288XemTEFYq25RK0IBGGdvYz9OqT2R3lblBQrXDjSi9WG16sGx 60MGhM2egGMqFQ5DBfT16IKw00+RjFgCVzJ8T64Lzw82E0e7d6hl39SPybY+uvrt SID60hYGmXoOdaiC9qquivks67BZprGNfORrvyJNrCFI6oKUFWHrQ1PpGd2tOwJN 1P3gkkS8pVlAif6ZQkAf+zuKu+l4j5tKxGlJAkJsafVJDLOxBKutUj5msha0g6uJ gFXUe0+G8hkNcEjd8XqUUCwIOY3pdv4WsydKBk3uH9zMnYolw53k1q0ObvoY1NXf beMxHQAtDi7nfQGlae9cuuOSymy95WuvzfhZFKdPWUe8lKN9QXFIWVoCFnOm8T3P +FNCUE+p8DIWkal6Ul9THi/Kz4p7twyrUp1LwT5EtSaJ3iGAmB9I+8/1vmZT3lPi nX8P+iVGM5yOUnptrsFm0bUcJWRD6iaTK1KxpH+Is4h2kiUiSz1tC/9bKaJYN2o9 oy7q7+ZVfHSmIxLo8ZFYsaZBcXi96cKuuPMR3X4ISPwKDqP5irxU/QbI+YQBMshg G4b0BNoMZ50g30r3Hcsifw4pzPQF0RDMOBeCiOi3AgMBAAGjNjA0MA4GA1UdDwEB /wQEAwIAoDAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDALBgkq hkiG9w0BAQsDggIBAFuS/VrMNUwEMyUIktDyna5ExYh/FDOE+YEYf8tsX7dSMhRK wE560/AcVZcbKKAZOnZ/262a++8tparsQt+bXBJ2so6YUqsFDNdOLCI2aShjWDRe TNhqmLIO3FNsLRKp96WHVz+jFoiECsoYfKn0jgqTqxx+7nWFqgBaNSlF5cbCgLCH jQV1uQhzsw/Mh/32hXAidkv/nLeLf7FbKq08hgthtoP+XstlzZ5BxkPodjb8XWXG DSS49SWX971GHa1apwMKfxVGSppxn18ZwEmW1BUfQBNxtMytqA9DK3+xuoUdXkB0 iJbm3Jc10JSRju8iyL121Xt6f8O33paVz/ndDJIWztUOjnItc89rxHsINPt5+cUt jix8ohwmHGDrK7ZooXBvotvmGT/xhPr2eHUAG8JuSJ/Cr09UUOwUEigz4CfgJOHm XukdzjOkb4r7lhNmVeGqrjRol1W0Wsc1NGH++J6xdkIeQ+i23kHwFHfQWV/J69tm rOn2N+qijtmbIy9YfVcrFDtUtEAzXylZ2StCVQNofd0M7tXNdrUL8yAFwlrhWGJV wsSP++1xH2Ie6Diupy8z6rbP383HmnmVPU/UecgLrlX2lEpt/UZkkX1Xm+6PhrrT HDeeULvqtUP3PD8wS0C873Pl9GXOKISqf0HKEIDUAVZhQOsGFqiZH0388M4L -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+localhost-key.pem000066400000000000000000000062571313450123100326660ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEAsF4L07p6z6ZK0OjBR3WSIzn1WSKfPN0eATbgu8qYbFDp8Oed PxL0vuLLOjFwlaW9Ie6l/Jla+KPRkjJFnbzxd6ZMQVirblErQgEYZ29jP06pPZHe VuUFCtcONKL1YbXqwbHrQwaEzZ6AYyoVDkMF9PXogrDTT5GMWAJXMnxPrgvPDzYT R7t3qGXf1I/Jtj66+u1IgPrSFgaZeg51qIL2qq6K+SzrsFmmsY185Gu/Ik2sIUjq gpQVYetDU+kZ3a07Ak3U/eCSRLylWUCJ/plCQB/7O4q76XiPm0rEaUkCQmxp9UkM s7EEq61SPmayFrSDq4mAVdR7T4byGQ1wSN3xepRQLAg5jel2/hazJ0oGTe4f3Myd iiXDneTWrQ5u+hjU1d9t4zEdAC0OLud9AaVp71y645LKbL3la6/N+FkUp09ZR7yU o31BcUhZWgIWc6bxPc/4U0JQT6nwMhaRqXpSX1MeL8rPinu3DKtSnUvBPkS1Jone IYCYH0j7z/W+ZlPeU+Kdfw/6JUYznI5Sem2uwWbRtRwlZEPqJpMrUrGkf4iziHaS JSJLPW0L/1spolg3aj2jLurv5lV8dKYjEujxkVixpkFxeL3pwq648xHdfghI/AoO o/mKvFT9Bsj5hAEyyGAbhvQE2gxnnSDfSvcdyyJ/DinM9AXREMw4F4KI6LcCAwEA AQKCAgEAnrHg/oD7ZMEC7PuifoRCHMRYCf5nPkLQbtNMYG2pvT0JY6VlDo4l/2Te 7NvzrBPYHSI55RKwkq4FMwFdNtP+imTulJYOm1MaE2gc52WI7jv/eNE6OQIWCWz8 8Uv4dBVWyTcos8S31rTaXWBOVejlAUgMERy+5wfWOpLQlzLYF4m0pMFJk/AReUtB nmhLXlsPsB22cag/RWZmzzcXk6tT/LzVe+R5ptLkdTsUuAxjjaBKVCDiMuDAZL1m dah3h8oKIMab8l0SABumxKqYAKkyvbSJQUhSUYAT5+3c0cfJ6q7WoMk8TqvnwfpQ 2Klbcaa4G6+79H8e/a41RWmcMVTTpLKmwzx/iMLPswLnTFbWYCsLSsml3OpmXPhG CKdbIWMvNMBfahZmnCP2pNcZBVY1/k/lEw25ehtnWqA7HplawT6V3gk/Bzz+3e3R XEpioZF70ipdW5Pb3OG/tKSNDvRRjqLPk9UWlQzmedzu7XN28V/blw/CBVcMAcc0 njwAledTuqv/wQ67dtbXdcxSPZbV/Rq7y3OmpgK6RWLIFzzpOPW5gULqUZfrnxtv StxVnlZXhFoymodFobTi7AYibsLaXLkunZWXEwFwdtLfFHznfHq/rHfBmna1lcKW MgWRqsbaoCsqHC1nc0E4llFkn3zqGYgMQNBeqNfX6cIPI/eQzPECggEBAOk0TP8N edIFENOrzUtpH1fB3k15heeA84SeBhj8t/xrphR3o+IVO/GtMtq9hVLeYFVPwWCi Mmy4KhwNUOtFeCSX4MbpiXvoPEjL3QF+Sv95HsEWsT1iBQIN4aoV0ZSv48YsRczs tLjr96hADLTMfpCwyRq9r8XVF/hnx7vqOoOC/J1kteRhjOWRnutFpdAMfkFgzUa9 1unmDHsDifcT+vpxief9Q9zK9xMYvYmwFkBUjOlhC7WchZC20nrwvM+A2mMBpeLB WSRWsYeOqW8zcQNGdWuXXMKxsYHwv9tXbANVWxs1gz4x7BxcFoN5poIFrnT+eImY EwhGrKR6jZsKF00CggEBAMGbdZU0+yvxL2tAul5RGAqv9xhdUV4eg8warTQ8/RWt 8Vef2wllBYnP48rXNDovb7ZNOjMBdjIWZ2zq2McMtHqpzP+zWQWaNT8/7Zi24JTL y4G75kZdGgTPG2Y71seZoZGxfOu4gf7cLKOqxiHYrNDHEDl5Pi13tJD/8qf6hYm6 K3yALSv+QlM3mk+5oueKQ7Lj9rV81YomYSV5+K+WhszhvLmuxv0necOLKapeBWvL GQ5038yAq3PFdu0HXzyA6L8YdusP1d3sqwQvLbi8KAMXJCeT6WZXGYgX2Rjfbuih ZHUaE7Ac0EsJfMuOowSkS7oXuT81k64ngCoq5KZC5hMCggEBAKYkt9JiZG8HYuSb GsjmHQllup5RvN+hVF0gRFHbAq2YeBtO3Xg+DpXxAjErIuhWPCWri6bwB6LDVmTj 68milaTke6TbTzLy0rg+Xbcppf766LlCFIYZ5l1/TE3j+4vGAC347sW/wkWY/7lj 4GmS43zsJmqhx6/XUJuOPJOZnZSCZr0vuhL6mOoZZDJUTXy62dx0PetvZsT/O9cM P2fDWWTCLTEVlBqik4KMdsS4qjGsyzOeCzyZReNDDRO/nZTsRSqSSwARJhQom5Rr RDVQXeyqbw93KAQhmshroBSB5Rc+4YiyCE3wPTo7NWL38XPi3lbF0VSd/rk/uNH5 6hcSCmUCggEAIPHjQFCTrRaNiyKolAQYozjuQyceAXYP11tyvcDjEB1ZRB/flemq 15iYmpukN4J67/qUPLmy8zL8xnvwB28SBw195MUQEPP8u5aVR7dW3/sN1jWzKaYO F2Nmti7YjX6HD9Oz/iiXdlbhAbi9nmTQg3ZcPGt1OSd1gncLQ6pNrvIPFFB7X1EU 2DRN/eMI5X2Rp49DG/7yF2AQh+AJgVeL+LEw/CfRlKJzBeNYY7U8Fuuoh907eAEt K7YeVpc6jYEiGeJ/2eAH9IuhTkT48saRyHTXoiR5QwDvR0lHmAPtS4irH4Igd4dv qlUi90B+XPvYJwKCc08aojf2hzZlUiVwIQKCAQEAraCoWea8hLFchxmAiBt7joIg nNK7a3LOHYxT1gB9H+PoVqTmzGVTeZpD8Jnis/UHmDhRYuUGqvFIefjAWbz0jJAN t6RMAozENCG1PoeXHf1gt2wspv14kza+8jSdpzNrzZgPZdb7Wh1UEqUkiRYwn87f C7DHknqCj9S2qq0DFXYz15JNPVrbvD+ZLBFJhTAjppS9TuYQVLf8JPYHpLRio/9A dMsyOz1VA2RRYN0u/u4ccxiN45K3PbVMCeDPbWXNm8G75YKQ7LnIuehMB1qkZy6N MOnNGp3l/ZkFK0JsW/pZqTQ2FqSkb0+ttTFApFI3qB04sc4s0uKPI9fa0OQtUw== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+localregistry-cert.pem000066400000000000000000000034321313450123100337160ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIFETCCAvugAwIBAgIQCnqSQalw9ytL5bHLgHZe+jALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE1MDUyNjIw NTQ1OFoXDTE4MDUxMDIwNTQ1OFowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV BAMTDWxvY2FscmVnaXN0cnkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC AQC9gvT3cwz0Ih9+7Ilv5lc15HsEiSmEMh4nOMZrSaamKgf/ydCiGo3DQapr/XDK FHMLKq68AxwfOlzmEFQ4d9umpPMQ2+4GBr0VG23ppGtQApIPHgD06S0/CeHmDIXN FXcKybPX/9KbgNkXBWbbJkJy0EcsdP8VJD50Q2WH89nvgEYJNFuKEELD3iGY6bBF jeDTle5jYA7CgBKvD2avn31g24Qhxn8n8/BdYO/U0kw0qmoy1veLOjCAW0os0jkM NlKrFpyHEWNj5B3X6UgSn8EGQaVbDq17PrQwlHJYU4nih0TnD1OwvBnFnd27pXjr 68eGA6Zc2BbUnhNGhppWHZ46LpPxpIbafSOH3ES3N/MZAfcUKIUntLlWE2xCQgFV TW95WeVtP/r1aWgIHu0E2Jb2eHCE+qXYqJxSU7S4DcknmmcTS69hzyHs+92Ec+7Q m0aQFZ0dyPoYPwXMgZpTAIuXEGg/FKC1fiS/deTW37DyvB2jppehKW3RJY3uso7R o9vs6DJx1OdU5XEq9R3n7op61N7PK8Wxmn7TVYHEZHkITVvtucZZd1FNTOrOJaNJ UnE+FuPK1Mrff+jz666Ru4zQL0CondOamX3QR5tuNK6MTqFs87wKY25qsqz7cS27 kHW+r7UNWbJY3/UQhaPZM78zCZa2IL1nBFUjsFvEA4rtYwIDAQABozowODAOBgNV HQ8BAf8EBAMCAKAwDAYDVR0TAQH/BAIwADAYBgNVHREEETAPgg1sb2NhbHJlZ2lz dHJ5MAsGCSqGSIb3DQEBCwOCAgEAHVGMyoyX4lRzWCDkUjrXkrDZzuv03M2ojW2Q UL61ejMkTWQW8R4gKrcPHAOJAPKVfGEVOrQH3ZMyxV2HnWrJ7egrn65zOzmLbWSh O7gdpL6YYjBr218fqJn/8HadXZa4k70JyympYOLojeWSLy3KP03U+y7AMcdE1uG6 6HJI54ZjBoW/nEyWmMh/mfMz8EN+Mgek48Z9AVaOswbtHtDIXN7XO0jbB3DbY5Yh prVqVLYAz4sCchGTadj+aEChF5sJkKREDvAew/njC0WGS2TmMJ+V1uVhXV6354mr edk79YvdwzwDgeYArkprahMtn9eu1aSTfUXsmM5OP5tR4gyFV1kUmTPY1yUd/yO+ 638wV0mWtGbbf6j8dUKeUBCyt2qGg8J80OUeFdvdHMswtaUq951NApX44BinPkbK moBVQByZ5OEcmMidFC9SqYSUwTQ7uNyWeguhCXav+l3x900YlKnUQgRUZntPwXjs yc7MXv0j0E86Gme6G1O02zamwkRgr3qOTHu2oQOow/a24fM4HASayLR0Kegt0sh3 rzk0HRF1mGonf1Ecyyj/3LpHVsgYSckwtJoZLOqtDMn+CKtOCEByssQfD+E9Qe07 qMyvcwpXUpfqe3ZERbJ10m98Z88VeK/XGt9ptq7HY47n1KL6lx3oyXwZIw8pq928 89dcqL0= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/ssl/registry-noca+localregistry-key.pem000066400000000000000000000062531313450123100335550ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKAIBAAKCAgEAvYL093MM9CIffuyJb+ZXNeR7BIkphDIeJzjGa0mmpioH/8nQ ohqNw0Gqa/1wyhRzCyquvAMcHzpc5hBUOHfbpqTzENvuBga9FRtt6aRrUAKSDx4A 9OktPwnh5gyFzRV3Csmz1//Sm4DZFwVm2yZCctBHLHT/FSQ+dENlh/PZ74BGCTRb ihBCw94hmOmwRY3g05XuY2AOwoASrw9mr599YNuEIcZ/J/PwXWDv1NJMNKpqMtb3 izowgFtKLNI5DDZSqxachxFjY+Qd1+lIEp/BBkGlWw6tez60MJRyWFOJ4odE5w9T sLwZxZ3du6V46+vHhgOmXNgW1J4TRoaaVh2eOi6T8aSG2n0jh9xEtzfzGQH3FCiF J7S5VhNsQkIBVU1veVnlbT/69WloCB7tBNiW9nhwhPql2KicUlO0uA3JJ5pnE0uv Yc8h7PvdhHPu0JtGkBWdHcj6GD8FzIGaUwCLlxBoPxSgtX4kv3Xk1t+w8rwdo6aX oSlt0SWN7rKO0aPb7OgycdTnVOVxKvUd5+6KetTezyvFsZp+01WBxGR5CE1b7bnG WXdRTUzqziWjSVJxPhbjytTK33/o8+uukbuM0C9AqJ3Tmpl90EebbjSujE6hbPO8 CmNuarKs+3Etu5B1vq+1DVmyWN/1EIWj2TO/MwmWtiC9ZwRVI7BbxAOK7WMCAwEA AQKCAgEArwqno2uEGnbuKnjmVRInmWKpcb4TN8Rm74lUVEKaB76o1s0cxK3MJP6h H8/e/vg2bqkE7indLsbkiaepcuLaYijXTcomJzDQMw+7zOOOLz/Aku/+qDg8D47c NXV5nLzn0HIPiEIF0JYJbmcR4veKxqu0Ic8K0QdCHHcn75P/x2Tuy4+twW9Vi76/ v5KRuxzZ/fTtVKKj32kWWNXb3fltgCoh+GR0jH2XlVh1DVkVBEwnfT/rM5ESvWwU riOah7ohT1+6QlOAPwKzwfr6FCG000eNKPb8q+p12q0ylHzMzgxtSxJwFb0X/Nzc snaboyWLjDAQ2I7LP6WmXizznvkKbE9PjW6UGYQ+2XApqp+Hn8tSC5I/gIDlBOOa psJ4fkRjr8n5+CbHbGmQG736hZcZY/z10TtOQbxeeeuri6oDQ62D4Z07GpWCG2EG sUakaytZnJkIN79PpfthPZwtStlG0KVs0i5wggH/iP2h0yAmvJ64ZRIqdvuE/aBn sdfRRlYUqmFOJsVQgtUWGKGS4WIxrGaclzT1TNxCKdiAk0glXe3sDtvBni6qDW07 iJzEXxrsLw6MiCDhHfDeae5JYeJXK0HlCfYHXgRmEnDFTGw8rBzwz3eXvPqZ5YNt j+31uHSwQjgOgEgSrXeTmRfLZsytKqndhBB/yBFmzZNrswXGackCggEBAMN5RSdW t+WWl8ghDGz/CN1oRjnk298/6L7ijluKGRgG+igwBEy+5m1EGPJT+Y5LEH4TiQJe Oc2XjQuM7zABX7JWWk1cL8Zlv3kcmR0lg4BWs7wDkoU1HYRkMP57vubtxFzFOsNa momivEniZ/eonHm3yv0VHeenH9j3mhJ3mVDIpkH+7uhn3++c0zYh96NkjfQi1/jF P35eSAt7FgHDOt37fWXwtGeYFRN4P19ZUNiIvZwT6Q1gmegRO8BYoW6cSbLWe5Cp abaULds46+mjM4zJhCZRFkdWHbzP4bZHocSmwGsqcpABJ6SASTVim02GGhBIt1nj fkqa10X1c5Sqis0CggEBAPgxFKSHccfIJ6yht2HJjysRLN/IHlO9hDcpCWUrISN/ hxu1uxfNGmUkd0H8zDO/O+QAJXLE8PPPB77pJniIJ8kK4swwsfufN6bNV9XJldjA o4vXnYt9Mpuky9cugD8LocUgWQzzKY5Y875TC4s3ldzyKQVm0NO+Wz1U3gfjogEC d7PhTk7Ba/ZjVGtL7HuZxlL+/TgZklMks2ulSTW2y8aqVJxaZXv0H0NX/+fpDHYw iljr+iqbiqZvjrzySryb0XWMtzP9oyDEXTXrWnG+kOIZW3BZ9FLxT+Te7zZ2PUbK vTkObsKxc8WVHIYgkt/OwWSwbYLre5nvFPvgEFbQuO8CggEAeZTlUXmbul63m5AK xYS/w88G1x2lMK/0mT4bY4562zoDwJlVI1MdydqwVZGryDiiUnjeIC3xcBISdZu8 bjR8jFUvp6xuPs2ska0bA0kBCQNkmc3zBY2rBVy4KKFZdRNwrm8yhK3HL1KcIKyF FEK4yPBrfozy49JMecxP9aqUHu4eky/4828gl04JBUONXwC9VpuRj7dILdaAozt0 zbXb2JSDQ7O60jCC83A4oprQMU6j+P9dVqe+Mtz9OD8ocb8eC/FiO/FTwm9aMl+u RMzw1GHHI3oODGLg7j6y2oilcsZxKnblePJu8N+mKWFizY5aicRg3rUkKU00Ftx7 fn2xBQKCAQB7w7Xgie5SStyF+KrC58kuF8WB3oBJEAOjoiIeQhCnbAvK5KfkqZHV CAc0b8TAtUc/XldOUSk6222oZQmbJ4J3fac1Xb8TlAUjd9iqMnk3+nBT5vSYP5mC Bf7kUjr/tWQ5MfVWQNfjNTZvHWhvRwvDfzq3h9rxDEbhYbXKx1fdGwboO51aJpgY 6NWLH/RQepFsh91sIUxXi8CxGF5Wm84oRn4k7esXkdgZNAPX+N4O/guvZhV9M81D S/QpAsYEIcuky8P7+Cplx6YXokKa4AXNyglQEHuG9PD7V7SAOxw5dhZAIpNXIThz OfVcaVf0pVzJQjWKCLW9QHz9UXG0aScfAoIBACdr3exVMUaMOtrAnf2NXj3hecgg WsWRBOOaSW5wXGt1JNlfYS4zwViafIwy31DNuMg22rj5Mq0TYMtuNYto5RoLSXeB uupUrENEBnt7JFrwI/NyWG0uYMM3G2MtGHGYooaT9+++wT96QxJZr5fwFYF1ddf6 5tFeKtNt5VM0wWBHO1voUhQ0TCaooatJjMuAB0+WbvwniKxmdbqQDzY+6myBBUVo gBJ0JxhxakLm1XGFHDtPCsAAHX/uZ4CvH2uyWqAlx6iwGXd0wwEGrbIRB/BundxR oaJWswU4FIPAgOpy2LEJKnvzhcmVFtZWD5sFXA1/83QvpceLTFTD5uioBPU= -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/test.passwd000066400000000000000000000000571313450123100262140ustar00rootroot00000000000000testuser:$apr1$YmLhHjm6$AjP4z8J1WgcUNxU8J4ue5. docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/v1/000077500000000000000000000000001313450123100243365ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/nginx/v1/search.json000066400000000000000000000005541313450123100265020ustar00rootroot00000000000000{"num_pages":1,"num_results":2,"page":1,"page_size": 25,"query":"testsearch","results":[{"description":"","is_automated":false,"is_official":false,"is_trusted":false, "name":"dmcgowan/testsearch-1","star_count":1000},{"description":"Some automated build","is_automated":true,"is_official":false,"is_trusted":false,"name":"dmcgowan/testsearch-2","star_count":10}]} docker-registry-2.6.2~ds1/contrib/docker-integration/run_multiversion.sh000077500000000000000000000034221313450123100266510ustar00rootroot00000000000000#!/usr/bin/env bash # Run the integration tests with multiple versions of the Docker engine set -e set -x DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) if [ "$TMPDIR" != "" ] && [ ! -d "$TMPDIR" ]; then mkdir -p $TMPDIR fi cachedir=`mktemp -t -d golem-cache.XXXXXX` trap "rm -rf $cachedir" EXIT if [ "$1" == "-d" ]; then # Drivers to use for Docker engines the tests are going to create. STORAGE_DRIVER=${STORAGE_DRIVER:-overlay} docker daemon --log-level=panic --storage-driver="$STORAGE_DRIVER" & DOCKER_PID=$! # Wait for it to become reachable. tries=10 until docker version &> /dev/null; do (( tries-- )) if [ $tries -le 0 ]; then echo >&2 "error: daemon failed to start" exit 1 fi sleep 1 done trap "kill $DOCKER_PID" EXIT fi distimage=$(docker build -q $DIR/../..) fullversion=$(git describe --match 'v[0-9]*' --dirty='.m' --always) distversion=${fullversion:1} echo "Testing image $distimage with distribution version $distversion" # Pull needed images before invoking golem to get pull time # These images are defined in golem.conf time docker pull nginx:1.9 time docker pull golang:1.6 time docker pull registry:0.9.1 time docker pull dmcgowan/token-server:simple time docker pull dmcgowan/token-server:oauth time docker pull distribution/golem-runner:0.1-bats time docker pull docker:1.9.1-dind time docker pull docker:1.10.3-dind time docker pull docker:1.11.1-dind golem -cache $cachedir \ -i "golem-distribution:latest,$distimage,$distversion" \ -i "golem-dind:latest,docker:1.9.1-dind,1.9.1" \ -i "golem-dind:latest,docker:1.10.3-dind,1.10.3" \ -i "golem-dind:latest,docker:1.11.1-dind,1.11.1" \ $DIR docker-registry-2.6.2~ds1/contrib/docker-integration/tls.bats000066400000000000000000000050421313450123100243430ustar00rootroot00000000000000#!/usr/bin/env bats # Registry host name, should be set to non-localhost address and match # DNS name in nginx/ssl certificates and what is installed in /etc/docker/cert.d load helpers hostname="localregistry" base="hello-world" image="${base}:latest" # Login information, should match values in nginx/test.passwd user=${TEST_USER:-"testuser"} password=${TEST_PASSWORD:-"passpassword"} email="distribution@docker.com" function setup() { tempImage $image } @test "Test valid certificates" { docker_t tag -f $image $hostname:5440/$image run docker_t push $hostname:5440/$image [ "$status" -eq 0 ] has_digest "$output" } @test "Test basic auth" { basic_auth_version_check login $hostname:5441 docker_t tag -f $image $hostname:5441/$image run docker_t push $hostname:5441/$image [ "$status" -eq 0 ] has_digest "$output" } @test "Test basic auth with build" { basic_auth_version_check login $hostname:5441 image1=$hostname:5441/$image-build image2=$hostname:5441/$image-build-2 tempImage $image1 run docker_t push $image1 [ "$status" -eq 0 ] has_digest "$output" docker_t rmi $image1 run build $image2 $image1 echo $output [ "$status" -eq 0 ] run docker_t push $image2 echo $output [ "$status" -eq 0 ] has_digest "$output" } @test "Test TLS client auth" { docker_t tag -f $image $hostname:5442/$image run docker_t push $hostname:5442/$image [ "$status" -eq 0 ] has_digest "$output" } @test "Test TLS client with invalid certificate authority fails" { docker_t tag -f $image $hostname:5443/$image run docker_t push $hostname:5443/$image [ "$status" -ne 0 ] } @test "Test basic auth with TLS client auth" { basic_auth_version_check login $hostname:5444 docker_t tag -f $image $hostname:5444/$image run docker_t push $hostname:5444/$image [ "$status" -eq 0 ] has_digest "$output" } @test "Test unknown certificate authority fails" { docker_t tag -f $image $hostname:5445/$image run docker_t push $hostname:5445/$image [ "$status" -ne 0 ] } @test "Test basic auth with unknown certificate authority fails" { run login $hostname:5446 [ "$status" -ne 0 ] docker_t tag -f $image $hostname:5446/$image run docker_t push $hostname:5446/$image [ "$status" -ne 0 ] } @test "Test TLS client auth to server with unknown certificate authority fails" { docker_t tag -f $image $hostname:5447/$image run docker_t push $hostname:5447/$image [ "$status" -ne 0 ] } @test "Test failure to connect to server fails to fallback to SSLv3" { docker_t tag -f $image $hostname:5448/$image run docker_t push $hostname:5448/$image [ "$status" -ne 0 ] } docker-registry-2.6.2~ds1/contrib/docker-integration/token.bats000066400000000000000000000057721313450123100246730ustar00rootroot00000000000000#!/usr/bin/env bats # This tests contacting a registry using a token server load helpers user="testuser" password="testpassword" email="a@nowhere.com" base="hello-world" @test "Test token server login" { run docker_t login -u $user -p $password -e $email localregistry:5554 echo $output [ "$status" -eq 0 ] # First line is WARNING about credential save or email deprecation [ "${lines[2]}" = "Login Succeeded" -o "${lines[1]}" = "Login Succeeded" ] } @test "Test token server bad login" { run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5554 [ "$status" -ne 0 ] run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5554 [ "$status" -ne 0 ] } @test "Test push and pull with token auth" { login localregistry:5555 image="localregistry:5555/testuser/token" build $image "$base:latest" run docker_t push $image echo $output [ "$status" -eq 0 ] docker_t rmi $image docker_t pull $image } @test "Test push and pull with token auth wrong namespace" { login localregistry:5555 image="localregistry:5555/notuser/token" build $image "$base:latest" run docker_t push $image [ "$status" -ne 0 ] } @test "Test oauth token server login" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" login_oauth localregistry:5557 } @test "Test oauth token server bad login" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" run docker_t login -u "testuser" -p "badpassword" -e $email localregistry:5557 [ "$status" -ne 0 ] run docker_t login -u "baduser" -p "testpassword" -e $email localregistry:5557 [ "$status" -ne 0 ] } @test "Test oauth push and pull with token auth" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" login_oauth localregistry:5558 image="localregistry:5558/testuser/token" build $image "$base:latest" run docker_t push $image echo $output [ "$status" -eq 0 ] docker_t rmi $image docker_t pull $image } @test "Test oauth push and build with token auth" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" login_oauth localregistry:5558 image="localregistry:5558/testuser/token-build" tempImage $image run docker_t push $image echo $output [ "$status" -eq 0 ] has_digest "$output" docker_t rmi $image image2="localregistry:5558/testuser/token-build-2" run build $image2 $image echo $output [ "$status" -eq 0 ] run docker_t push $image2 echo $output [ "$status" -eq 0 ] has_digest "$output" } @test "Test oauth push and pull with token auth wrong namespace" { version_check docker "$GOLEM_DIND_VERSION" "1.11.0" login_oauth localregistry:5558 image="localregistry:5558/notuser/token" build $image "$base:latest" run docker_t push $image [ "$status" -ne 0 ] } @test "Test oauth with v1 search" { version_check docker "$GOLEM_DIND_VERSION" "1.12.0" run docker_t search localregistry:5600/testsearch [ "$status" -ne 0 ] login_oauth localregistry:5600 run docker_t search localregistry:5600/testsearch echo $output [ "$status" -eq 0 ] echo $output | grep "testsearch-1" echo $output | grep "testsearch-2" } docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/000077500000000000000000000000001313450123100263525ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/.htpasswd000066400000000000000000000001061313450123100302050ustar00rootroot00000000000000testuser:$2y$05$T2MlBvkN1R/yICNnLuf1leOlOfAY0DvybctbbWUFKlojfkShVgn4m docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/Dockerfile000066400000000000000000000003101313450123100303360ustar00rootroot00000000000000FROM dmcgowan/token-server:oauth WORKDIR / COPY ./.htpasswd /.htpasswd COPY ./certs/auth.localregistry.cert /tls.cert COPY ./certs/auth.localregistry.key /tls.key COPY ./certs/signing.key /sign.key docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/000077500000000000000000000000001313450123100274725ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/auth.localregistry.cert000066400000000000000000000021631313450123100341760ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDHDCCAgagAwIBAgIRAKhhQMnqZx+hkOmoUYgPb+kwCwYJKoZIhvcNAQELMCYx ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw MDQyMzFaFw0xOTAxMTIwMDQyMzFaMDAxETAPBgNVBAoTCFF1aWNrVExTMRswGQYD VQQDExJhdXRoLmxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQD1tUf1EghBlIRrE83yF4zDgRu7vH2Jo0kygKJUWtQQe+DfXyjjE/fg FdKnnoEjsIeF9hxNbTt0ldDz7/n97pbMhoiXULi9iq4jlgSzVL2XEAgrON0YSY/c Lmmd1KSa/pOUZr2WMAYPZ+FdQfE1W7SMNbErPefBqYdFzpZ+esAtvbajYwIjl8Vy 9c4bidx4vgnNrR9GcFYibjC5sj8syh/OtbzzqiVGT8YcPpmMG6KNRkausa4gqpon NKYG8C3WDaiPCLYKcvFrFfdEWF/m2oj14eXACXT9iwp8r4bsLgXrZwqcpKOWfVRu qHC8aV476EYgxWCAOANExUdUaRt5wL/jAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIA oDAMBgNVHRMBAf8EAjAAMB0GA1UdEQQWMBSCEmF1dGgubG9jYWxyZWdpc3RyeTAL BgkqhkiG9w0BAQsDggEBABxPGK9FdGDxcLowNsExKnnZvmQT3H0u+Dux1gkp0AhH KOrmx3LUENUKLSgotzx133tgOgR5lzAWVFy7bhLwlPhOslxf2oEfztsAMd/tY8rW PrG2ZqYqlzEQQ9INbAc3woo5A3slN07uhP3F16jNqoMM4zRmw6Ba70CluGKT7x5+ xVjKoWITLjWDXT5m35PnsN8CpBaFzXYcod/5p9XwCFp0s+aNxfpZECCV/3yqIr+J ALzroPh43FAlG96o4NyYZ2Msp63newN19R2+TgpV4nXuw2mLVDpvetP7RRqnpvj/ qwRgt5j4hFjJWb61M0ELL7A9fA71h1ImdGCvnArdBQs= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/auth.localregistry.key000066400000000000000000000032171313450123100340320ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA9bVH9RIIQZSEaxPN8heMw4Ebu7x9iaNJMoCiVFrUEHvg318o 4xP34BXSp56BI7CHhfYcTW07dJXQ8+/5/e6WzIaIl1C4vYquI5YEs1S9lxAIKzjd GEmP3C5pndSkmv6TlGa9ljAGD2fhXUHxNVu0jDWxKz3nwamHRc6WfnrALb22o2MC I5fFcvXOG4nceL4Jza0fRnBWIm4wubI/LMofzrW886olRk/GHD6ZjBuijUZGrrGu IKqaJzSmBvAt1g2ojwi2CnLxaxX3RFhf5tqI9eHlwAl0/YsKfK+G7C4F62cKnKSj ln1UbqhwvGleO+hGIMVggDgDRMVHVGkbecC/4wIDAQABAoIBAQCrsjXKRwOF8CZo PLqZBWPT6hBbK+f9miC4LbNBhwbRTf9hl7mWlImOCTHe95/+NIk/Ty+P21jEqzwM ehETJPoziX9BXaL6sEHnlBlMx1aEjStoKKA3LJBeqAAdzk4IEQVHmlO4824IreqJ pF7Njnunzo0zTlr4tWJVoXsAfv5z9tNtdkxYBbIa0fjfGtlqXU3gLq58FCON3mB/ NGc0AyA1UFGp0FzpdEcwTGD4InsXbcmsl2l/VPBJuZbryITRqWs6BbK++80DRhNt afMhP+IzKrWSCp0rBYrqqz6AevtlKdEfQK1yXPEjN/63QLMevt8mF/1JCp//TQnf Z6bIQbAhAoGBAP7vFA0PcvoXt9MXvvAwrKY1s6pNw4nWPG27qY1/m+DkBwP8IQms 4AWGv1wscZzXJYTvaLO5/qjmGUj50ohcVEvyZJioh1pKXA8Chxvd6rBA/O/Lj5E0 3MOSA5Q0gxJ0Mhv0zGbbyN5fY8D8zhxoqQP4LoW+UdZG2Oi6JxsQ9c9dAoGBAPa8 U3bGuM5OGA9EWP7mkB/VnjDTL1aEIN3cOHbHIKwH/loxdYcNMBE7vwxV1CzgIzXT wsL0iE15fQdK938u0+um8aH5QtbWNI8tdk1XVjEC/i3C7N6WVUutneCKUDb4QxiB 9OvWCbNNiN+xTKBBM93YlwO3GYfrW9Pmm9q1+hg/AoGBALJlUS22gun50PxaIJZq KVcCO2DQnCYHki/j48mN4+HjD/m85M2lePrFCYIR48syTyIQer9SR5+frVAA6k/b 9G1VCQo+3MDVSkiCp1Nb3tBKGfYgB65ARMBinDiI6rPuNeaUTrkn0g+yxtaU0hLV Nnj9omia/x+oYj+xjI4HN0xNAoGARy92dSJIV104m88ATip/EnAzP6ruUWu1f8z1 jW9OAdQckjEK03f+kjpGmGx61qekAPejjVO3r4KJi/0ZAtyjz61OsYiUvB748wYO x6mW+HUAmHtQk7eTzE2+6vV8xx9BXGTCIPiTu+N2xfMFRIcLS8odZ7j/6LMCv1Qd SzCNg0kCgYBaNlEs4pK1VxZZpEWwVmFpgIxfEfxLIaGrek6wBTcCn/VA2M0oHuez mlMio8VY0yWPBJz30JflDiTmYIvteLPMHT0N0J6isiXLhzJSFI4+cAMLE2Q5v8rz W+W5/L8YZeierW0qJat1BrgStaf5ZLpiOc9pKBSwycydPH5BfVdK/A== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/ca.pem000066400000000000000000000020761313450123100305650ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIQNS9SaFSFBN7Zvwjalrf2DDALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE2MDEyODAw NDIzMFoXDTE5MDExMjAwNDIzMFowJjERMA8GA1UEChMIUXVpY2tUTFMxETAPBgNV BAMTCFF1aWNrVExTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/Pf fQ7VUTSXs12PRyrLDVDz7kPDbGNTt0vF7FYDmTTGOU3i62xZNOGuxBezAiVSV5A3 lopwsv4OH7DRtSaPn+XCt1JDALna2WrjT0MshypMd5o2c3jmGUfAKf5gjizgIoEl d4e5aqEBuOQP+QCEde+8p8N1buQW+zMy9srM2O/7BFMIaQ07CWLlj3hIiF+L5rKD L6dWtKT7INRmRwpuZZnThEWnBSNgayrWek6G0i3y8QYTfVA1SwA+H3grJxy5NrLp GYXSmu2509mu0QAHhx05t1rJhwhFz/4sG7j8AggYeDXEqfQ/VIb/bvnW9bD+vrQ2 ZnICvxnzNMYBx23BkQIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/ BAUwAwEB/zALBgkqhkiG9w0BAQsDggEBALvTi6E44Fltu83dFLVEj0kLtusI/TTH Tw6upoB5pRG+7A75w0Ii8bvvd2tNpBOg+L+80xyIFqaNkXhLKTN4lgtd7WiCuyb/ w1BEuF/+RjCXhu6wQ/63ab46d6ctaQ1zjxlU2rQLQXQFALI8ntyn/TELc01HYkr2 x3NHlbnBNlgI2CKXPeUBzvBylTCcdYGwoa+2ZPdIsFjle2aCIBoZ+WNZlIbFwgLh XCHwcbviC+thjqOneJpJZmRW9AxQ638ki6iGItdrJewCN/1dcL2KKjxnC5VHbpne SOjEPNXihY08Brl8myhFNtRRKZ55MJIYzDtVQSkCaT91Q3XX9tSZadY= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/localregistry.cert000066400000000000000000000021431313450123100332340ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfugAwIBAgIQN7rT95eAy75c4n6/AsDJODALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE2MDEyODAw NDIzMloXDTE5MDExMjAwNDIzMlowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV BAMTDWxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQDLi75QEkl/qekcoOJlNv9y1IXvrbU2ssl4ViJiZRjWx+/CkyCCOyf9YUpAgRLr Pskqde2mwhuNP8yBlOBb17Sapz7N3+hJi5j9vLBAFcamPeF3PqxjFv7j5TKkRmSI dFYQclREwMUd3qEH322KkqOnsEEfdmCgFqWORe+QR5AxzxQP3Pnd4OYH1yZCh0MQ P2pJgrxxf2I5I/m1AUgoHV1cdBbCv9LGohJPpMtwPC0dJpgMFcnf6hT37At236AY V437HiRruY7iPWkYFrSPWpwdslJ32MZvRN5RS163jZXjiZ7qWnQOiiDJfXe4evB/ yQLN4m0qVQxsMz7rkY7OsqaXAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIAoDAMBgNV HRMBAf8EAjAAMBgGA1UdEQQRMA+CDWxvY2FscmVnaXN0cnkwCwYJKoZIhvcNAQEL A4IBAQAyUb3EuMaOylBeV8+4KeBiE4lxykDOwLLSk3jXRsVVtfJpX3v8l5vwo/Jf iG8tzzz+7uiskI96u3TsekUtVkUxujfKevMP+369K/59s7NRmwwlFMyB2fvL14B2 oweVjWvM/8fZl6irtFdbJFXXRm7paKso5cmfImxhojAwohgcd4XTVLE/7juYa582 AaBdRuIiyL71MU9qa1mC5+57AaSLPYaPKpahemgYYkV1Z403Kd6rXchxdQ8JIAL8 +0oYTSC+svnz1tUU/V5E5id9LQaTmDN5iIVFhNpqAaZmR45UI86woWvnkMb8Ants 4aknwTwY3300PuTqBdQufvOFDRN5 -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/localregistry.key000066400000000000000000000032171313450123100330720ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAy4u+UBJJf6npHKDiZTb/ctSF7621NrLJeFYiYmUY1sfvwpMg gjsn/WFKQIES6z7JKnXtpsIbjT/MgZTgW9e0mqc+zd/oSYuY/bywQBXGpj3hdz6s Yxb+4+UypEZkiHRWEHJURMDFHd6hB99tipKjp7BBH3ZgoBaljkXvkEeQMc8UD9z5 3eDmB9cmQodDED9qSYK8cX9iOSP5tQFIKB1dXHQWwr/SxqIST6TLcDwtHSaYDBXJ 3+oU9+wLdt+gGFeN+x4ka7mO4j1pGBa0j1qcHbJSd9jGb0TeUUtet42V44me6lp0 DoogyX13uHrwf8kCzeJtKlUMbDM+65GOzrKmlwIDAQABAoIBAF6vFMp+lz4RteSh Wm8m1FGAVwWVUpStOlcGClynFpTi0L88XYT3K7UMStQSttBDlqRv0ysdZF+ia+lj bbKLdvHyFp8CJzX/AB4YZgyJlKzEYFtuBhbaHZu5hIMyU5W+OELSTCznV0p7w4C8 CGLLr+FTdhfCo1QU9NJn6fa9s2/XRdSClBBalAHYs0ZS7ZckaF/sPiC/VapfBMet qjJXNYiO6pXYriGWKF9zdAMfk2CM0BVWbnwQZkMSEQirrTcJwm3ezyloXCv2nywK /VzbUT1HJVyzo5oAwTd0MwDc2oEMiFzlfO028zY4LDltpia+SyWvFi5NaIqzFESc yLgJacECgYEA3jvH+ZQHQf42Md8TCciokaYvwWIKJdk4WRjbvE5cBZekyXAm7/3b /1VFDKsy2RPlfmfHP3wy9rlnjzsRveB5qaclgS8aI67AYsWd/yRgfRatl7Ve9bHl LY6VM5L/DZTxykcqivwjc77XoDuBfUKs6tyuSLQku+FOTbLtNYlUCHECgYEA6nkR lkXufyLmDhNb3093RsYvPcs1kGaIIGTnz3cxWNh485DgsyLBuYQ5ugupQkzM8YSt ohDTmVpggqjlXQxCg0Zw8gkEV0v8KsLGjn1CuTJg/mBArXlelq1FEeRAYC9/YfOz ocXegHV7wDKKtcraNZFsEc7Z0LwbC9wtzSFG44cCgYASkMX1CLPOhJE8e1lY0OWc PVjx++HDJbF6aAQ7aARyBygiF/d4xylw3EvHcinuTqY2eC8CE7siN3z6T0H9Ldqc HLWaZDf30SqLVd0MKprQ+GsKKIHFXtY5hxbZ1ybtmIrWjjl0oPnJOqFC5pW7xC0z 9bmtozcKZxkmjpMYjN9zUQKBgQCqV6KLRerqunPgLfhE1/qTlE+l2QflDFhBEI3I j5NuNHZKnSphehK7sHAv1WD2Jc2OeRGb+BWCB8Ktqf5YBxwbOwW7EQnyUeW1OyP9 SMs8uHj21P6oCNDLLr5LLUQHnPoyM1aBZLstICzziMR1JhY5bJjSpzBfEQmlKCSu LkrN6QKBgQCRXrBJRUxeJj7wCnCSq0Clf9NhCpQnwo4bEx8sKlj8K8ku8MvwQwoM 3KfWc7bOl6A2/mM/k4yoHtBMM9X9xqYtsgeFhxuiWBcfTmTxWh73LQ48Kgbrgodt 6yTccnjr7OtBidD85c6lgjAUgcL43QY8mlw0OhzXAZ2R5HWFp4ht+w== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/signing.cert000066400000000000000000000020761313450123100320140ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIRAJ6IIisIZxL86oe3oeoAgWUwCwYJKoZIhvcNAQELMCYx ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw MDQyMzNaFw0xOTAxMTIwMDQyMzNaMBMxETAPBgNVBAoTCFF1aWNrVExTMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IXUwqSdO2QTj2ET6fJPGe+KWVnt QCQQWjkWVpOz8L2A29BRvv9z6lYNf9sOM0Xb5IUAgoZ/s3U6LNYT/RWYFBfeo40r Xd/MNKAn0kFsSb6BIKmUwPqFeqc8wiPX6yY4SbF1sUTkCTkw3yFHg/AIlwmhpFH3 9mAmV+x0kTzFR/78ZDD5CUNS59bbu+7UqB06YrJuVEwPY98YixSPXTcaKimsUe+K IY8FQ6yN6l27MK56wlj4hw2gYz+cyBUBCExCgYMQlOSg2ilH4qYyFvccSDUH7jTA NwpsIBfdoUVbI+j2ivn+ZGD614LtIStGgUu0mDDVxVOWnRvq/z7LMaa2jwIDAQAB ozUwMzAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0T AQH/BAIwADALBgkqhkiG9w0BAQsDggEBAJq3JzTLrIWCF8rHLTTm1icE9PjOO0sV a1wrmdJ6NwRbJ66dLZ/4G/NZjVOnce9WFHYLFSEG+wx5YVUPuJXpJaSdy0h8F0Uw hiJwgeVsGg7vcf4G6mWHrsauDOhylnD31UtYPX1Ao/jcntyyf+gCQpY1J/B8l1yU LNOwvWLVLpZwZ4ehbKA/UnDXgA+3uHvpzl//cPe0cnt+Mhrgzk5mIMwVR6zCZw1G oVutAHpv2PXxRwTMu51J+QtSL2b2w3mGHxDLpmz8UdXOtkxdpmDT8kIOtX0T5yGL 29F3fa81iZPs02GWjSGOfOzmCCvaA4C5KJvY/WulF7OOgwvrBpQwqTI= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/certs/signing.key000066400000000000000000000032131313450123100316410ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA3IXUwqSdO2QTj2ET6fJPGe+KWVntQCQQWjkWVpOz8L2A29BR vv9z6lYNf9sOM0Xb5IUAgoZ/s3U6LNYT/RWYFBfeo40rXd/MNKAn0kFsSb6BIKmU wPqFeqc8wiPX6yY4SbF1sUTkCTkw3yFHg/AIlwmhpFH39mAmV+x0kTzFR/78ZDD5 CUNS59bbu+7UqB06YrJuVEwPY98YixSPXTcaKimsUe+KIY8FQ6yN6l27MK56wlj4 hw2gYz+cyBUBCExCgYMQlOSg2ilH4qYyFvccSDUH7jTANwpsIBfdoUVbI+j2ivn+ ZGD614LtIStGgUu0mDDVxVOWnRvq/z7LMaa2jwIDAQABAoIBAD2tiNZv6DImSXo+ sq0qQomEf/OBvWPFMnWppd/NK/TXa+UPHO4I0MjoDJqIEC6zCU+fC4d2St1MmlrT /X85vPFRw8mGwGxfHeRSLxEVj04I5GDYTWy0JQUrJUk/cTKp2/Bwm/RaylTyFAM0 caYrSpvD69vjuTDFr7PDxM6iaqM53zK/vD8kCe81z+wN0UbAKsLlUOKztjH6SzL9 uVOkekIT/j3L2xxyQhjmhfA3TuCP4uNK/+6/4ovl9Nj4pQsFomsCk4phgqy9SOm1 4yufmVd8k7J3cppMlMPNc+7tqe2Xn593Y8QT95y3yhtkFECF70yBw64HMDDpA22p 5b/JV9ECgYEA9H4RBXOwbdjcpCa9H3mFjHqUQCqNme1vOSGiflZh9KBCDKgdqugm KHpvAECADie0p6XRHpxRvufKnGFkJwedfeiKz51T+0dqgPxWncYT1TC+cAjOSzfM wBpUOcAyvTTviwGbg4bLanHo4remzCbcnRvHQX4YfPFCjT9GhsU+XEUCgYEA5ubz IlSu1wwFJpoO24ZykGUyqGUQXzR0NrXiLrpF0764qjmHyF8SPJPv1XegSxP/nUTz SjVfJ7wye/X9qlOpBY8mzy9qQMMKc1cQBV1yVW8IRZ7pMYQZO7qmrZD/DWTa5qWt pqSbIH2FKedELsKJA/SBtczKjspOdDKyh0UelsMCgYA7DyTfc0XAEy2hPXZb3wgC mi2rnlvcPf2rCFPvPsCkzf2GfynDehaVmpWrsuj8Al1iTezI/yvD+Mv5oJEH2JAT tROq+S8rOOIiTFJEBHAQBJlMCOSESPNdyD5mQOZAzEO9CWNejzYd/WwrL//Luut5 zBcC3AngTIsuAYXw0j6xHQKBgQDamkAJep7k3W5q82OplgoUhpqFLtlnKSP1QBFZ J+U/6Mqv7jONEeUUEQL42H6bVd2kqUikMw9ZcSVikquLfBUDPFoDwOIZWg4k0IJM cgHyvGHad+5SgLva/oUawbGWnqtXvfc/U4vCINPXrimxE1/grLW4xp/mu8W24OCA jIG/PQKBgD/Apl+sfqiB/6ONBjjIswA4yFkEXHSZNpAgcPwhA+cO5D0afEWz2HIx VeOh5NjN1EL0hX8clFW4bfkK1Vr0kjvbMUXnBWaibUgpiVQl9O9WjaKQLZrp4sRu x2kJ07Qn6ri7f/lsqOELZwBy95iHWRdePptaAKkRGxJstHI7dgUt -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/registry-config-notls.yml000066400000000000000000000005611313450123100333470ustar00rootroot00000000000000version: 0.1 loglevel: debug storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /tmp/registry-dev http: addr: 0.0.0.0:5000 auth: token: realm: "https://auth.localregistry:5559/token/" issuer: "registry-test" service: "registry-test" rootcertbundle: "/etc/docker/registry/tokenbundle.pem" docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver-oauth/registry-config.yml000066400000000000000000000007571313450123100322210ustar00rootroot00000000000000version: 0.1 loglevel: debug storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /tmp/registry-dev http: addr: 0.0.0.0:5000 tls: certificate: "/etc/docker/registry/localregistry.cert" key: "/etc/docker/registry/localregistry.key" auth: token: realm: "https://auth.localregistry:5559/token/" issuer: "registry-test" service: "registry-test" rootcertbundle: "/etc/docker/registry/tokenbundle.pem" docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/000077500000000000000000000000001313450123100252345ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/.htpasswd000066400000000000000000000001061313450123100270670ustar00rootroot00000000000000testuser:$2y$05$T2MlBvkN1R/yICNnLuf1leOlOfAY0DvybctbbWUFKlojfkShVgn4m docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/Dockerfile000066400000000000000000000003111313450123100272210ustar00rootroot00000000000000FROM dmcgowan/token-server:simple WORKDIR / COPY ./.htpasswd /.htpasswd COPY ./certs/auth.localregistry.cert /tls.cert COPY ./certs/auth.localregistry.key /tls.key COPY ./certs/signing.key /sign.key docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/000077500000000000000000000000001313450123100263545ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/auth.localregistry.cert000066400000000000000000000021631313450123100330600ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDHDCCAgagAwIBAgIRAKhhQMnqZx+hkOmoUYgPb+kwCwYJKoZIhvcNAQELMCYx ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw MDQyMzFaFw0xOTAxMTIwMDQyMzFaMDAxETAPBgNVBAoTCFF1aWNrVExTMRswGQYD VQQDExJhdXRoLmxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw ggEKAoIBAQD1tUf1EghBlIRrE83yF4zDgRu7vH2Jo0kygKJUWtQQe+DfXyjjE/fg FdKnnoEjsIeF9hxNbTt0ldDz7/n97pbMhoiXULi9iq4jlgSzVL2XEAgrON0YSY/c Lmmd1KSa/pOUZr2WMAYPZ+FdQfE1W7SMNbErPefBqYdFzpZ+esAtvbajYwIjl8Vy 9c4bidx4vgnNrR9GcFYibjC5sj8syh/OtbzzqiVGT8YcPpmMG6KNRkausa4gqpon NKYG8C3WDaiPCLYKcvFrFfdEWF/m2oj14eXACXT9iwp8r4bsLgXrZwqcpKOWfVRu qHC8aV476EYgxWCAOANExUdUaRt5wL/jAgMBAAGjPzA9MA4GA1UdDwEB/wQEAwIA oDAMBgNVHRMBAf8EAjAAMB0GA1UdEQQWMBSCEmF1dGgubG9jYWxyZWdpc3RyeTAL BgkqhkiG9w0BAQsDggEBABxPGK9FdGDxcLowNsExKnnZvmQT3H0u+Dux1gkp0AhH KOrmx3LUENUKLSgotzx133tgOgR5lzAWVFy7bhLwlPhOslxf2oEfztsAMd/tY8rW PrG2ZqYqlzEQQ9INbAc3woo5A3slN07uhP3F16jNqoMM4zRmw6Ba70CluGKT7x5+ xVjKoWITLjWDXT5m35PnsN8CpBaFzXYcod/5p9XwCFp0s+aNxfpZECCV/3yqIr+J ALzroPh43FAlG96o4NyYZ2Msp63newN19R2+TgpV4nXuw2mLVDpvetP7RRqnpvj/ qwRgt5j4hFjJWb61M0ELL7A9fA71h1ImdGCvnArdBQs= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/auth.localregistry.key000066400000000000000000000032171313450123100327140ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEA9bVH9RIIQZSEaxPN8heMw4Ebu7x9iaNJMoCiVFrUEHvg318o 4xP34BXSp56BI7CHhfYcTW07dJXQ8+/5/e6WzIaIl1C4vYquI5YEs1S9lxAIKzjd GEmP3C5pndSkmv6TlGa9ljAGD2fhXUHxNVu0jDWxKz3nwamHRc6WfnrALb22o2MC I5fFcvXOG4nceL4Jza0fRnBWIm4wubI/LMofzrW886olRk/GHD6ZjBuijUZGrrGu IKqaJzSmBvAt1g2ojwi2CnLxaxX3RFhf5tqI9eHlwAl0/YsKfK+G7C4F62cKnKSj ln1UbqhwvGleO+hGIMVggDgDRMVHVGkbecC/4wIDAQABAoIBAQCrsjXKRwOF8CZo PLqZBWPT6hBbK+f9miC4LbNBhwbRTf9hl7mWlImOCTHe95/+NIk/Ty+P21jEqzwM ehETJPoziX9BXaL6sEHnlBlMx1aEjStoKKA3LJBeqAAdzk4IEQVHmlO4824IreqJ pF7Njnunzo0zTlr4tWJVoXsAfv5z9tNtdkxYBbIa0fjfGtlqXU3gLq58FCON3mB/ NGc0AyA1UFGp0FzpdEcwTGD4InsXbcmsl2l/VPBJuZbryITRqWs6BbK++80DRhNt afMhP+IzKrWSCp0rBYrqqz6AevtlKdEfQK1yXPEjN/63QLMevt8mF/1JCp//TQnf Z6bIQbAhAoGBAP7vFA0PcvoXt9MXvvAwrKY1s6pNw4nWPG27qY1/m+DkBwP8IQms 4AWGv1wscZzXJYTvaLO5/qjmGUj50ohcVEvyZJioh1pKXA8Chxvd6rBA/O/Lj5E0 3MOSA5Q0gxJ0Mhv0zGbbyN5fY8D8zhxoqQP4LoW+UdZG2Oi6JxsQ9c9dAoGBAPa8 U3bGuM5OGA9EWP7mkB/VnjDTL1aEIN3cOHbHIKwH/loxdYcNMBE7vwxV1CzgIzXT wsL0iE15fQdK938u0+um8aH5QtbWNI8tdk1XVjEC/i3C7N6WVUutneCKUDb4QxiB 9OvWCbNNiN+xTKBBM93YlwO3GYfrW9Pmm9q1+hg/AoGBALJlUS22gun50PxaIJZq KVcCO2DQnCYHki/j48mN4+HjD/m85M2lePrFCYIR48syTyIQer9SR5+frVAA6k/b 9G1VCQo+3MDVSkiCp1Nb3tBKGfYgB65ARMBinDiI6rPuNeaUTrkn0g+yxtaU0hLV Nnj9omia/x+oYj+xjI4HN0xNAoGARy92dSJIV104m88ATip/EnAzP6ruUWu1f8z1 jW9OAdQckjEK03f+kjpGmGx61qekAPejjVO3r4KJi/0ZAtyjz61OsYiUvB748wYO x6mW+HUAmHtQk7eTzE2+6vV8xx9BXGTCIPiTu+N2xfMFRIcLS8odZ7j/6LMCv1Qd SzCNg0kCgYBaNlEs4pK1VxZZpEWwVmFpgIxfEfxLIaGrek6wBTcCn/VA2M0oHuez mlMio8VY0yWPBJz30JflDiTmYIvteLPMHT0N0J6isiXLhzJSFI4+cAMLE2Q5v8rz W+W5/L8YZeierW0qJat1BrgStaf5ZLpiOc9pKBSwycydPH5BfVdK/A== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/ca.pem000066400000000000000000000020761313450123100274470ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIQNS9SaFSFBN7Zvwjalrf2DDALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE2MDEyODAw NDIzMFoXDTE5MDExMjAwNDIzMFowJjERMA8GA1UEChMIUXVpY2tUTFMxETAPBgNV BAMTCFF1aWNrVExTMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu/Pf fQ7VUTSXs12PRyrLDVDz7kPDbGNTt0vF7FYDmTTGOU3i62xZNOGuxBezAiVSV5A3 lopwsv4OH7DRtSaPn+XCt1JDALna2WrjT0MshypMd5o2c3jmGUfAKf5gjizgIoEl d4e5aqEBuOQP+QCEde+8p8N1buQW+zMy9srM2O/7BFMIaQ07CWLlj3hIiF+L5rKD L6dWtKT7INRmRwpuZZnThEWnBSNgayrWek6G0i3y8QYTfVA1SwA+H3grJxy5NrLp GYXSmu2509mu0QAHhx05t1rJhwhFz/4sG7j8AggYeDXEqfQ/VIb/bvnW9bD+vrQ2 ZnICvxnzNMYBx23BkQIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAKQwDwYDVR0TAQH/ BAUwAwEB/zALBgkqhkiG9w0BAQsDggEBALvTi6E44Fltu83dFLVEj0kLtusI/TTH Tw6upoB5pRG+7A75w0Ii8bvvd2tNpBOg+L+80xyIFqaNkXhLKTN4lgtd7WiCuyb/ w1BEuF/+RjCXhu6wQ/63ab46d6ctaQ1zjxlU2rQLQXQFALI8ntyn/TELc01HYkr2 x3NHlbnBNlgI2CKXPeUBzvBylTCcdYGwoa+2ZPdIsFjle2aCIBoZ+WNZlIbFwgLh XCHwcbviC+thjqOneJpJZmRW9AxQ638ki6iGItdrJewCN/1dcL2KKjxnC5VHbpne SOjEPNXihY08Brl8myhFNtRRKZ55MJIYzDtVQSkCaT91Q3XX9tSZadY= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/localregistry.cert000066400000000000000000000021431313450123100321160ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfugAwIBAgIQN7rT95eAy75c4n6/AsDJODALBgkqhkiG9w0BAQswJjER MA8GA1UEChMIUXVpY2tUTFMxETAPBgNVBAMTCFF1aWNrVExTMB4XDTE2MDEyODAw NDIzMloXDTE5MDExMjAwNDIzMlowKzERMA8GA1UEChMIUXVpY2tUTFMxFjAUBgNV BAMTDWxvY2FscmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB AQDLi75QEkl/qekcoOJlNv9y1IXvrbU2ssl4ViJiZRjWx+/CkyCCOyf9YUpAgRLr Pskqde2mwhuNP8yBlOBb17Sapz7N3+hJi5j9vLBAFcamPeF3PqxjFv7j5TKkRmSI dFYQclREwMUd3qEH322KkqOnsEEfdmCgFqWORe+QR5AxzxQP3Pnd4OYH1yZCh0MQ P2pJgrxxf2I5I/m1AUgoHV1cdBbCv9LGohJPpMtwPC0dJpgMFcnf6hT37At236AY V437HiRruY7iPWkYFrSPWpwdslJ32MZvRN5RS163jZXjiZ7qWnQOiiDJfXe4evB/ yQLN4m0qVQxsMz7rkY7OsqaXAgMBAAGjOjA4MA4GA1UdDwEB/wQEAwIAoDAMBgNV HRMBAf8EAjAAMBgGA1UdEQQRMA+CDWxvY2FscmVnaXN0cnkwCwYJKoZIhvcNAQEL A4IBAQAyUb3EuMaOylBeV8+4KeBiE4lxykDOwLLSk3jXRsVVtfJpX3v8l5vwo/Jf iG8tzzz+7uiskI96u3TsekUtVkUxujfKevMP+369K/59s7NRmwwlFMyB2fvL14B2 oweVjWvM/8fZl6irtFdbJFXXRm7paKso5cmfImxhojAwohgcd4XTVLE/7juYa582 AaBdRuIiyL71MU9qa1mC5+57AaSLPYaPKpahemgYYkV1Z403Kd6rXchxdQ8JIAL8 +0oYTSC+svnz1tUU/V5E5id9LQaTmDN5iIVFhNpqAaZmR45UI86woWvnkMb8Ants 4aknwTwY3300PuTqBdQufvOFDRN5 -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/localregistry.key000066400000000000000000000032171313450123100317540ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpAIBAAKCAQEAy4u+UBJJf6npHKDiZTb/ctSF7621NrLJeFYiYmUY1sfvwpMg gjsn/WFKQIES6z7JKnXtpsIbjT/MgZTgW9e0mqc+zd/oSYuY/bywQBXGpj3hdz6s Yxb+4+UypEZkiHRWEHJURMDFHd6hB99tipKjp7BBH3ZgoBaljkXvkEeQMc8UD9z5 3eDmB9cmQodDED9qSYK8cX9iOSP5tQFIKB1dXHQWwr/SxqIST6TLcDwtHSaYDBXJ 3+oU9+wLdt+gGFeN+x4ka7mO4j1pGBa0j1qcHbJSd9jGb0TeUUtet42V44me6lp0 DoogyX13uHrwf8kCzeJtKlUMbDM+65GOzrKmlwIDAQABAoIBAF6vFMp+lz4RteSh Wm8m1FGAVwWVUpStOlcGClynFpTi0L88XYT3K7UMStQSttBDlqRv0ysdZF+ia+lj bbKLdvHyFp8CJzX/AB4YZgyJlKzEYFtuBhbaHZu5hIMyU5W+OELSTCznV0p7w4C8 CGLLr+FTdhfCo1QU9NJn6fa9s2/XRdSClBBalAHYs0ZS7ZckaF/sPiC/VapfBMet qjJXNYiO6pXYriGWKF9zdAMfk2CM0BVWbnwQZkMSEQirrTcJwm3ezyloXCv2nywK /VzbUT1HJVyzo5oAwTd0MwDc2oEMiFzlfO028zY4LDltpia+SyWvFi5NaIqzFESc yLgJacECgYEA3jvH+ZQHQf42Md8TCciokaYvwWIKJdk4WRjbvE5cBZekyXAm7/3b /1VFDKsy2RPlfmfHP3wy9rlnjzsRveB5qaclgS8aI67AYsWd/yRgfRatl7Ve9bHl LY6VM5L/DZTxykcqivwjc77XoDuBfUKs6tyuSLQku+FOTbLtNYlUCHECgYEA6nkR lkXufyLmDhNb3093RsYvPcs1kGaIIGTnz3cxWNh485DgsyLBuYQ5ugupQkzM8YSt ohDTmVpggqjlXQxCg0Zw8gkEV0v8KsLGjn1CuTJg/mBArXlelq1FEeRAYC9/YfOz ocXegHV7wDKKtcraNZFsEc7Z0LwbC9wtzSFG44cCgYASkMX1CLPOhJE8e1lY0OWc PVjx++HDJbF6aAQ7aARyBygiF/d4xylw3EvHcinuTqY2eC8CE7siN3z6T0H9Ldqc HLWaZDf30SqLVd0MKprQ+GsKKIHFXtY5hxbZ1ybtmIrWjjl0oPnJOqFC5pW7xC0z 9bmtozcKZxkmjpMYjN9zUQKBgQCqV6KLRerqunPgLfhE1/qTlE+l2QflDFhBEI3I j5NuNHZKnSphehK7sHAv1WD2Jc2OeRGb+BWCB8Ktqf5YBxwbOwW7EQnyUeW1OyP9 SMs8uHj21P6oCNDLLr5LLUQHnPoyM1aBZLstICzziMR1JhY5bJjSpzBfEQmlKCSu LkrN6QKBgQCRXrBJRUxeJj7wCnCSq0Clf9NhCpQnwo4bEx8sKlj8K8ku8MvwQwoM 3KfWc7bOl6A2/mM/k4yoHtBMM9X9xqYtsgeFhxuiWBcfTmTxWh73LQ48Kgbrgodt 6yTccnjr7OtBidD85c6lgjAUgcL43QY8mlw0OhzXAZ2R5HWFp4ht+w== -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/signing.cert000066400000000000000000000020761313450123100306760ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIC9TCCAd+gAwIBAgIRAJ6IIisIZxL86oe3oeoAgWUwCwYJKoZIhvcNAQELMCYx ETAPBgNVBAoTCFF1aWNrVExTMREwDwYDVQQDEwhRdWlja1RMUzAeFw0xNjAxMjgw MDQyMzNaFw0xOTAxMTIwMDQyMzNaMBMxETAPBgNVBAoTCFF1aWNrVExTMIIBIjAN BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3IXUwqSdO2QTj2ET6fJPGe+KWVnt QCQQWjkWVpOz8L2A29BRvv9z6lYNf9sOM0Xb5IUAgoZ/s3U6LNYT/RWYFBfeo40r Xd/MNKAn0kFsSb6BIKmUwPqFeqc8wiPX6yY4SbF1sUTkCTkw3yFHg/AIlwmhpFH3 9mAmV+x0kTzFR/78ZDD5CUNS59bbu+7UqB06YrJuVEwPY98YixSPXTcaKimsUe+K IY8FQ6yN6l27MK56wlj4hw2gYz+cyBUBCExCgYMQlOSg2ilH4qYyFvccSDUH7jTA NwpsIBfdoUVbI+j2ivn+ZGD614LtIStGgUu0mDDVxVOWnRvq/z7LMaa2jwIDAQAB ozUwMzAOBgNVHQ8BAf8EBAMCAKAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0T AQH/BAIwADALBgkqhkiG9w0BAQsDggEBAJq3JzTLrIWCF8rHLTTm1icE9PjOO0sV a1wrmdJ6NwRbJ66dLZ/4G/NZjVOnce9WFHYLFSEG+wx5YVUPuJXpJaSdy0h8F0Uw hiJwgeVsGg7vcf4G6mWHrsauDOhylnD31UtYPX1Ao/jcntyyf+gCQpY1J/B8l1yU LNOwvWLVLpZwZ4ehbKA/UnDXgA+3uHvpzl//cPe0cnt+Mhrgzk5mIMwVR6zCZw1G oVutAHpv2PXxRwTMu51J+QtSL2b2w3mGHxDLpmz8UdXOtkxdpmDT8kIOtX0T5yGL 29F3fa81iZPs02GWjSGOfOzmCCvaA4C5KJvY/WulF7OOgwvrBpQwqTI= -----END CERTIFICATE----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/certs/signing.key000066400000000000000000000032131313450123100305230ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA3IXUwqSdO2QTj2ET6fJPGe+KWVntQCQQWjkWVpOz8L2A29BR vv9z6lYNf9sOM0Xb5IUAgoZ/s3U6LNYT/RWYFBfeo40rXd/MNKAn0kFsSb6BIKmU wPqFeqc8wiPX6yY4SbF1sUTkCTkw3yFHg/AIlwmhpFH39mAmV+x0kTzFR/78ZDD5 CUNS59bbu+7UqB06YrJuVEwPY98YixSPXTcaKimsUe+KIY8FQ6yN6l27MK56wlj4 hw2gYz+cyBUBCExCgYMQlOSg2ilH4qYyFvccSDUH7jTANwpsIBfdoUVbI+j2ivn+ ZGD614LtIStGgUu0mDDVxVOWnRvq/z7LMaa2jwIDAQABAoIBAD2tiNZv6DImSXo+ sq0qQomEf/OBvWPFMnWppd/NK/TXa+UPHO4I0MjoDJqIEC6zCU+fC4d2St1MmlrT /X85vPFRw8mGwGxfHeRSLxEVj04I5GDYTWy0JQUrJUk/cTKp2/Bwm/RaylTyFAM0 caYrSpvD69vjuTDFr7PDxM6iaqM53zK/vD8kCe81z+wN0UbAKsLlUOKztjH6SzL9 uVOkekIT/j3L2xxyQhjmhfA3TuCP4uNK/+6/4ovl9Nj4pQsFomsCk4phgqy9SOm1 4yufmVd8k7J3cppMlMPNc+7tqe2Xn593Y8QT95y3yhtkFECF70yBw64HMDDpA22p 5b/JV9ECgYEA9H4RBXOwbdjcpCa9H3mFjHqUQCqNme1vOSGiflZh9KBCDKgdqugm KHpvAECADie0p6XRHpxRvufKnGFkJwedfeiKz51T+0dqgPxWncYT1TC+cAjOSzfM wBpUOcAyvTTviwGbg4bLanHo4remzCbcnRvHQX4YfPFCjT9GhsU+XEUCgYEA5ubz IlSu1wwFJpoO24ZykGUyqGUQXzR0NrXiLrpF0764qjmHyF8SPJPv1XegSxP/nUTz SjVfJ7wye/X9qlOpBY8mzy9qQMMKc1cQBV1yVW8IRZ7pMYQZO7qmrZD/DWTa5qWt pqSbIH2FKedELsKJA/SBtczKjspOdDKyh0UelsMCgYA7DyTfc0XAEy2hPXZb3wgC mi2rnlvcPf2rCFPvPsCkzf2GfynDehaVmpWrsuj8Al1iTezI/yvD+Mv5oJEH2JAT tROq+S8rOOIiTFJEBHAQBJlMCOSESPNdyD5mQOZAzEO9CWNejzYd/WwrL//Luut5 zBcC3AngTIsuAYXw0j6xHQKBgQDamkAJep7k3W5q82OplgoUhpqFLtlnKSP1QBFZ J+U/6Mqv7jONEeUUEQL42H6bVd2kqUikMw9ZcSVikquLfBUDPFoDwOIZWg4k0IJM cgHyvGHad+5SgLva/oUawbGWnqtXvfc/U4vCINPXrimxE1/grLW4xp/mu8W24OCA jIG/PQKBgD/Apl+sfqiB/6ONBjjIswA4yFkEXHSZNpAgcPwhA+cO5D0afEWz2HIx VeOh5NjN1EL0hX8clFW4bfkK1Vr0kjvbMUXnBWaibUgpiVQl9O9WjaKQLZrp4sRu x2kJ07Qn6ri7f/lsqOELZwBy95iHWRdePptaAKkRGxJstHI7dgUt -----END RSA PRIVATE KEY----- docker-registry-2.6.2~ds1/contrib/docker-integration/tokenserver/registry-config.yml000066400000000000000000000007571313450123100311030ustar00rootroot00000000000000version: 0.1 loglevel: debug storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /tmp/registry-dev http: addr: 0.0.0.0:5000 tls: certificate: "/etc/docker/registry/localregistry.cert" key: "/etc/docker/registry/localregistry.key" auth: token: realm: "https://auth.localregistry:5556/token/" issuer: "registry-test" service: "registry-test" rootcertbundle: "/etc/docker/registry/tokenbundle.pem" docker-registry-2.6.2~ds1/contrib/token-server/000077500000000000000000000000001313450123100215215ustar00rootroot00000000000000docker-registry-2.6.2~ds1/contrib/token-server/errors.go000066400000000000000000000023751313450123100233730ustar00rootroot00000000000000package main import ( "net/http" "github.com/docker/distribution/registry/api/errcode" ) var ( errGroup = "tokenserver" // ErrorBadTokenOption is returned when a token parameter is invalid ErrorBadTokenOption = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "BAD_TOKEN_OPTION", Message: "bad token option", Description: `This error may be returned when a request for a token contains an option which is not valid`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorMissingRequiredField is returned when a required form field is missing ErrorMissingRequiredField = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MISSING_REQUIRED_FIELD", Message: "missing required field", Description: `This error may be returned when a request for a token does not contain a required form field`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorUnsupportedValue is returned when a form field has an unsupported value ErrorUnsupportedValue = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "UNSUPPORTED_VALUE", Message: "unsupported value", Description: `This error may be returned when a request for a token contains a form field with an unsupported value`, HTTPStatusCode: http.StatusBadRequest, }) ) docker-registry-2.6.2~ds1/contrib/token-server/main.go000066400000000000000000000274261313450123100230070ustar00rootroot00000000000000package main import ( "encoding/json" "flag" "math/rand" "net/http" "strconv" "strings" "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/auth" _ "github.com/docker/distribution/registry/auth/htpasswd" "github.com/docker/libtrust" "github.com/gorilla/mux" ) var ( enforceRepoClass bool ) func main() { var ( issuer = &TokenIssuer{} pkFile string addr string debug bool err error passwdFile string realm string cert string certKey string ) flag.StringVar(&issuer.Issuer, "issuer", "distribution-token-server", "Issuer string for token") flag.StringVar(&pkFile, "key", "", "Private key file") flag.StringVar(&addr, "addr", "localhost:8080", "Address to listen on") flag.BoolVar(&debug, "debug", false, "Debug mode") flag.StringVar(&passwdFile, "passwd", ".htpasswd", "Passwd file") flag.StringVar(&realm, "realm", "", "Authentication realm") flag.StringVar(&cert, "tlscert", "", "Certificate file for TLS") flag.StringVar(&certKey, "tlskey", "", "Certificate key for TLS") flag.BoolVar(&enforceRepoClass, "enforce-class", false, "Enforce policy for single repository class") flag.Parse() if debug { logrus.SetLevel(logrus.DebugLevel) } if pkFile == "" { issuer.SigningKey, err = libtrust.GenerateECP256PrivateKey() if err != nil { logrus.Fatalf("Error generating private key: %v", err) } logrus.Debugf("Using newly generated key with id %s", issuer.SigningKey.KeyID()) } else { issuer.SigningKey, err = libtrust.LoadKeyFile(pkFile) if err != nil { logrus.Fatalf("Error loading key file %s: %v", pkFile, err) } logrus.Debugf("Loaded private key with id %s", issuer.SigningKey.KeyID()) } if realm == "" { logrus.Fatalf("Must provide realm") } ac, err := auth.GetAccessController("htpasswd", map[string]interface{}{ "realm": realm, "path": passwdFile, }) if err != nil { logrus.Fatalf("Error initializing access controller: %v", err) } // TODO: Make configurable issuer.Expiration = 15 * time.Minute ctx := context.Background() ts := &tokenServer{ issuer: issuer, accessController: ac, refreshCache: map[string]refreshToken{}, } router := mux.NewRouter() router.Path("/token/").Methods("GET").Handler(handlerWithContext(ctx, ts.getToken)) router.Path("/token/").Methods("POST").Handler(handlerWithContext(ctx, ts.postToken)) if cert == "" { err = http.ListenAndServe(addr, router) } else if certKey == "" { logrus.Fatalf("Must provide certficate (-tlscert) and key (-tlskey)") } else { err = http.ListenAndServeTLS(addr, cert, certKey, router) } if err != nil { logrus.Infof("Error serving: %v", err) } } // handlerWithContext wraps the given context-aware handler by setting up the // request context from a base context. func handlerWithContext(ctx context.Context, handler func(context.Context, http.ResponseWriter, *http.Request)) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithRequest(ctx, r) logger := context.GetRequestLogger(ctx) ctx = context.WithLogger(ctx, logger) handler(ctx, w, r) }) } func handleError(ctx context.Context, err error, w http.ResponseWriter) { ctx, w = context.WithResponseWriter(ctx, w) if serveErr := errcode.ServeJSON(w, err); serveErr != nil { context.GetResponseLogger(ctx).Errorf("error sending error response: %v", serveErr) return } context.GetResponseLogger(ctx).Info("application error") } var refreshCharacters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") const refreshTokenLength = 15 func newRefreshToken() string { s := make([]rune, refreshTokenLength) for i := range s { s[i] = refreshCharacters[rand.Intn(len(refreshCharacters))] } return string(s) } type refreshToken struct { subject string service string } type tokenServer struct { issuer *TokenIssuer accessController auth.AccessController refreshCache map[string]refreshToken } type tokenResponse struct { Token string `json:"access_token"` RefreshToken string `json:"refresh_token,omitempty"` ExpiresIn int `json:"expires_in,omitempty"` } var repositoryClassCache = map[string]string{} func filterAccessList(ctx context.Context, scope string, requestedAccessList []auth.Access) []auth.Access { if !strings.HasSuffix(scope, "/") { scope = scope + "/" } grantedAccessList := make([]auth.Access, 0, len(requestedAccessList)) for _, access := range requestedAccessList { if access.Type == "repository" { if !strings.HasPrefix(access.Name, scope) { context.GetLogger(ctx).Debugf("Resource scope not allowed: %s", access.Name) continue } if enforceRepoClass { if class, ok := repositoryClassCache[access.Name]; ok { if class != access.Class { context.GetLogger(ctx).Debugf("Different repository class: %q, previously %q", access.Class, class) continue } } else if strings.EqualFold(access.Action, "push") { repositoryClassCache[access.Name] = access.Class } } } else if access.Type == "registry" { if access.Name != "catalog" { context.GetLogger(ctx).Debugf("Unknown registry resource: %s", access.Name) continue } // TODO: Limit some actions to "admin" users } else { context.GetLogger(ctx).Debugf("Skipping unsupported resource type: %s", access.Type) continue } grantedAccessList = append(grantedAccessList, access) } return grantedAccessList } type acctSubject struct{} func (acctSubject) String() string { return "acctSubject" } type requestedAccess struct{} func (requestedAccess) String() string { return "requestedAccess" } type grantedAccess struct{} func (grantedAccess) String() string { return "grantedAccess" } // getToken handles authenticating the request and authorizing access to the // requested scopes. func (ts *tokenServer) getToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { context.GetLogger(ctx).Info("getToken") params := r.URL.Query() service := params.Get("service") scopeSpecifiers := params["scope"] var offline bool if offlineStr := params.Get("offline_token"); offlineStr != "" { var err error offline, err = strconv.ParseBool(offlineStr) if err != nil { handleError(ctx, ErrorBadTokenOption.WithDetail(err), w) return } } requestedAccessList := ResolveScopeSpecifiers(ctx, scopeSpecifiers) authorizedCtx, err := ts.accessController.Authorized(ctx, requestedAccessList...) if err != nil { challenge, ok := err.(auth.Challenge) if !ok { handleError(ctx, err, w) return } // Get response context. ctx, w = context.WithResponseWriter(ctx, w) challenge.SetHeaders(w) handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail(challenge.Error()), w) context.GetResponseLogger(ctx).Info("get token authentication challenge") return } ctx = authorizedCtx username := context.GetStringValue(ctx, "auth.user.name") ctx = context.WithValue(ctx, acctSubject{}, username) ctx = context.WithLogger(ctx, context.GetLogger(ctx, acctSubject{})) context.GetLogger(ctx).Info("authenticated client") ctx = context.WithValue(ctx, requestedAccess{}, requestedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, requestedAccess{})) grantedAccessList := filterAccessList(ctx, username, requestedAccessList) ctx = context.WithValue(ctx, grantedAccess{}, grantedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, grantedAccess{})) token, err := ts.issuer.CreateJWT(username, service, grantedAccessList) if err != nil { handleError(ctx, err, w) return } context.GetLogger(ctx).Info("authorized client") response := tokenResponse{ Token: token, ExpiresIn: int(ts.issuer.Expiration.Seconds()), } if offline { response.RefreshToken = newRefreshToken() ts.refreshCache[response.RefreshToken] = refreshToken{ subject: username, service: service, } } ctx, w = context.WithResponseWriter(ctx, w) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) context.GetResponseLogger(ctx).Info("get token complete") } type postTokenResponse struct { Token string `json:"access_token"` Scope string `json:"scope,omitempty"` ExpiresIn int `json:"expires_in,omitempty"` IssuedAt string `json:"issued_at,omitempty"` RefreshToken string `json:"refresh_token,omitempty"` } // postToken handles authenticating the request and authorizing access to the // requested scopes. func (ts *tokenServer) postToken(ctx context.Context, w http.ResponseWriter, r *http.Request) { grantType := r.PostFormValue("grant_type") if grantType == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing grant_type value"), w) return } service := r.PostFormValue("service") if service == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing service value"), w) return } clientID := r.PostFormValue("client_id") if clientID == "" { handleError(ctx, ErrorMissingRequiredField.WithDetail("missing client_id value"), w) return } var offline bool switch r.PostFormValue("access_type") { case "", "online": case "offline": offline = true default: handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown access_type value"), w) return } requestedAccessList := ResolveScopeList(ctx, r.PostFormValue("scope")) var subject string var rToken string switch grantType { case "refresh_token": rToken = r.PostFormValue("refresh_token") if rToken == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing refresh_token value"), w) return } rt, ok := ts.refreshCache[rToken] if !ok || rt.service != service { handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid refresh token"), w) return } subject = rt.subject case "password": ca, ok := ts.accessController.(auth.CredentialAuthenticator) if !ok { handleError(ctx, ErrorUnsupportedValue.WithDetail("password grant type not supported"), w) return } subject = r.PostFormValue("username") if subject == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing username value"), w) return } password := r.PostFormValue("password") if password == "" { handleError(ctx, ErrorUnsupportedValue.WithDetail("missing password value"), w) return } if err := ca.AuthenticateUser(subject, password); err != nil { handleError(ctx, errcode.ErrorCodeUnauthorized.WithDetail("invalid credentials"), w) return } default: handleError(ctx, ErrorUnsupportedValue.WithDetail("unknown grant_type value"), w) return } ctx = context.WithValue(ctx, acctSubject{}, subject) ctx = context.WithLogger(ctx, context.GetLogger(ctx, acctSubject{})) context.GetLogger(ctx).Info("authenticated client") ctx = context.WithValue(ctx, requestedAccess{}, requestedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, requestedAccess{})) grantedAccessList := filterAccessList(ctx, subject, requestedAccessList) ctx = context.WithValue(ctx, grantedAccess{}, grantedAccessList) ctx = context.WithLogger(ctx, context.GetLogger(ctx, grantedAccess{})) token, err := ts.issuer.CreateJWT(subject, service, grantedAccessList) if err != nil { handleError(ctx, err, w) return } context.GetLogger(ctx).Info("authorized client") response := postTokenResponse{ Token: token, ExpiresIn: int(ts.issuer.Expiration.Seconds()), IssuedAt: time.Now().UTC().Format(time.RFC3339), Scope: ToScopeList(grantedAccessList), } if offline { rToken = newRefreshToken() ts.refreshCache[rToken] = refreshToken{ subject: subject, service: service, } } if rToken != "" { response.RefreshToken = rToken } ctx, w = context.WithResponseWriter(ctx, w) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) context.GetResponseLogger(ctx).Info("post token complete") } docker-registry-2.6.2~ds1/contrib/token-server/token.go000066400000000000000000000136671313450123100232050ustar00rootroot00000000000000package main import ( "crypto" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "io" "regexp" "strings" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" "github.com/docker/distribution/registry/auth/token" "github.com/docker/libtrust" ) // ResolveScopeSpecifiers converts a list of scope specifiers from a token // request's `scope` query parameters into a list of standard access objects. func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Access { requestedAccessSet := make(map[auth.Access]struct{}, 2*len(scopeSpecs)) for _, scopeSpecifier := range scopeSpecs { // There should be 3 parts, separated by a `:` character. parts := strings.SplitN(scopeSpecifier, ":", 3) if len(parts) != 3 { context.GetLogger(ctx).Infof("ignoring unsupported scope format %s", scopeSpecifier) continue } resourceType, resourceName, actions := parts[0], parts[1], parts[2] resourceType, resourceClass := splitResourceClass(resourceType) if resourceType == "" { continue } // Actions should be a comma-separated list of actions. for _, action := range strings.Split(actions, ",") { requestedAccess := auth.Access{ Resource: auth.Resource{ Type: resourceType, Class: resourceClass, Name: resourceName, }, Action: action, } // Add this access to the requested access set. requestedAccessSet[requestedAccess] = struct{}{} } } requestedAccessList := make([]auth.Access, 0, len(requestedAccessSet)) for requestedAccess := range requestedAccessSet { requestedAccessList = append(requestedAccessList, requestedAccess) } return requestedAccessList } var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`) func splitResourceClass(t string) (string, string) { matches := typeRegexp.FindStringSubmatch(t) if len(matches) < 2 { return "", "" } if len(matches) == 2 || len(matches[2]) < 2 { return matches[1], "" } return matches[1], matches[2][1 : len(matches[2])-1] } // ResolveScopeList converts a scope list from a token request's // `scope` parameter into a list of standard access objects. func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { scopes := strings.Split(scopeList, " ") return ResolveScopeSpecifiers(ctx, scopes) } func scopeString(a auth.Access) string { if a.Class != "" { return fmt.Sprintf("%s(%s):%s:%s", a.Type, a.Class, a.Name, a.Action) } return fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action) } // ToScopeList converts a list of access to a // scope list string func ToScopeList(access []auth.Access) string { var s []string for _, a := range access { s = append(s, scopeString(a)) } return strings.Join(s, ",") } // TokenIssuer represents an issuer capable of generating JWT tokens type TokenIssuer struct { Issuer string SigningKey libtrust.PrivateKey Expiration time.Duration } // CreateJWT creates and signs a JSON Web Token for the given subject and // audience with the granted access. func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAccessList []auth.Access) (string, error) { // Make a set of access entries to put in the token's claimset. resourceActionSets := make(map[auth.Resource]map[string]struct{}, len(grantedAccessList)) for _, access := range grantedAccessList { actionSet, exists := resourceActionSets[access.Resource] if !exists { actionSet = map[string]struct{}{} resourceActionSets[access.Resource] = actionSet } actionSet[access.Action] = struct{}{} } accessEntries := make([]*token.ResourceActions, 0, len(resourceActionSets)) for resource, actionSet := range resourceActionSets { actions := make([]string, 0, len(actionSet)) for action := range actionSet { actions = append(actions, action) } accessEntries = append(accessEntries, &token.ResourceActions{ Type: resource.Type, Class: resource.Class, Name: resource.Name, Actions: actions, }) } randomBytes := make([]byte, 15) _, err := io.ReadFull(rand.Reader, randomBytes) if err != nil { return "", err } randomID := base64.URLEncoding.EncodeToString(randomBytes) now := time.Now() signingHash := crypto.SHA256 var alg string switch issuer.SigningKey.KeyType() { case "RSA": alg = "RS256" case "EC": alg = "ES256" default: panic(fmt.Errorf("unsupported signing key type %q", issuer.SigningKey.KeyType())) } joseHeader := token.Header{ Type: "JWT", SigningAlg: alg, } if x5c := issuer.SigningKey.GetExtendedField("x5c"); x5c != nil { joseHeader.X5c = x5c.([]string) } else { var jwkMessage json.RawMessage jwkMessage, err = issuer.SigningKey.PublicKey().MarshalJSON() if err != nil { return "", err } joseHeader.RawJWK = &jwkMessage } exp := issuer.Expiration if exp == 0 { exp = 5 * time.Minute } claimSet := token.ClaimSet{ Issuer: issuer.Issuer, Subject: subject, Audience: audience, Expiration: now.Add(exp).Unix(), NotBefore: now.Unix(), IssuedAt: now.Unix(), JWTID: randomID, Access: accessEntries, } var ( joseHeaderBytes []byte claimSetBytes []byte ) if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil { return "", fmt.Errorf("unable to encode jose header: %s", err) } if claimSetBytes, err = json.Marshal(claimSet); err != nil { return "", fmt.Errorf("unable to encode claim set: %s", err) } encodedJoseHeader := joseBase64Encode(joseHeaderBytes) encodedClaimSet := joseBase64Encode(claimSetBytes) encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet) var signatureBytes []byte if signatureBytes, _, err = issuer.SigningKey.Sign(strings.NewReader(encodingToSign), signingHash); err != nil { return "", fmt.Errorf("unable to sign jwt payload: %s", err) } signature := joseBase64Encode(signatureBytes) return fmt.Sprintf("%s.%s", encodingToSign, signature), nil } func joseBase64Encode(data []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") } docker-registry-2.6.2~ds1/coverpkg.sh000077500000000000000000000007041313450123100176150ustar00rootroot00000000000000#!/usr/bin/env bash # Given a subpackage and the containing package, figures out which packages # need to be passed to `go test -coverpkg`: this includes all of the # subpackage's dependencies within the containing package, as well as the # subpackage itself. DEPENDENCIES="$(go list -f $'{{range $f := .Deps}}{{$f}}\n{{end}}' ${1} | grep ${2} | grep -v github.com/docker/distribution/vendor)" echo "${1} ${DEPENDENCIES}" | xargs echo -n | tr ' ' ',' docker-registry-2.6.2~ds1/digest/000077500000000000000000000000001313450123100167145ustar00rootroot00000000000000docker-registry-2.6.2~ds1/digest/digest.go000066400000000000000000000074561313450123100205360ustar00rootroot00000000000000package digest import ( "fmt" "hash" "io" "regexp" "strings" ) const ( // DigestSha256EmptyTar is the canonical sha256 digest of empty data DigestSha256EmptyTar = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ) // Digest allows simple protection of hex formatted digest strings, prefixed // by their algorithm. Strings of type Digest have some guarantee of being in // the correct format and it provides quick access to the components of a // digest string. // // The following is an example of the contents of Digest types: // // sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc // // This allows to abstract the digest behind this type and work only in those // terms. type Digest string // NewDigest returns a Digest from alg and a hash.Hash object. func NewDigest(alg Algorithm, h hash.Hash) Digest { return NewDigestFromBytes(alg, h.Sum(nil)) } // NewDigestFromBytes returns a new digest from the byte contents of p. // Typically, this can come from hash.Hash.Sum(...) or xxx.SumXXX(...) // functions. This is also useful for rebuilding digests from binary // serializations. func NewDigestFromBytes(alg Algorithm, p []byte) Digest { return Digest(fmt.Sprintf("%s:%x", alg, p)) } // NewDigestFromHex returns a Digest from alg and a the hex encoded digest. func NewDigestFromHex(alg, hex string) Digest { return Digest(fmt.Sprintf("%s:%s", alg, hex)) } // DigestRegexp matches valid digest types. var DigestRegexp = regexp.MustCompile(`[a-zA-Z0-9-_+.]+:[a-fA-F0-9]+`) // DigestRegexpAnchored matches valid digest types, anchored to the start and end of the match. var DigestRegexpAnchored = regexp.MustCompile(`^` + DigestRegexp.String() + `$`) var ( // ErrDigestInvalidFormat returned when digest format invalid. ErrDigestInvalidFormat = fmt.Errorf("invalid checksum digest format") // ErrDigestInvalidLength returned when digest has invalid length. ErrDigestInvalidLength = fmt.Errorf("invalid checksum digest length") // ErrDigestUnsupported returned when the digest algorithm is unsupported. ErrDigestUnsupported = fmt.Errorf("unsupported digest algorithm") ) // ParseDigest parses s and returns the validated digest object. An error will // be returned if the format is invalid. func ParseDigest(s string) (Digest, error) { d := Digest(s) return d, d.Validate() } // FromReader returns the most valid digest for the underlying content using // the canonical digest algorithm. func FromReader(rd io.Reader) (Digest, error) { return Canonical.FromReader(rd) } // FromBytes digests the input and returns a Digest. func FromBytes(p []byte) Digest { return Canonical.FromBytes(p) } // Validate checks that the contents of d is a valid digest, returning an // error if not. func (d Digest) Validate() error { s := string(d) if !DigestRegexpAnchored.MatchString(s) { return ErrDigestInvalidFormat } i := strings.Index(s, ":") if i < 0 { return ErrDigestInvalidFormat } // case: "sha256:" with no hex. if i+1 == len(s) { return ErrDigestInvalidFormat } switch algorithm := Algorithm(s[:i]); algorithm { case SHA256, SHA384, SHA512: if algorithm.Size()*2 != len(s[i+1:]) { return ErrDigestInvalidLength } break default: return ErrDigestUnsupported } return nil } // Algorithm returns the algorithm portion of the digest. This will panic if // the underlying digest is not in a valid format. func (d Digest) Algorithm() Algorithm { return Algorithm(d[:d.sepIndex()]) } // Hex returns the hex digest portion of the digest. This will panic if the // underlying digest is not in a valid format. func (d Digest) Hex() string { return string(d[d.sepIndex()+1:]) } func (d Digest) String() string { return string(d) } func (d Digest) sepIndex() int { i := strings.Index(string(d), ":") if i < 0 { panic("could not find ':' in digest: " + d) } return i } docker-registry-2.6.2~ds1/digest/digest_test.go000066400000000000000000000041151313450123100215620ustar00rootroot00000000000000package digest import ( "testing" ) func TestParseDigest(t *testing.T) { for _, testcase := range []struct { input string err error algorithm Algorithm hex string }{ { input: "sha256:e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", algorithm: "sha256", hex: "e58fcf7418d4390dec8e8fb69d88c06ec07039d651fedd3aa72af9972e7d046b", }, { input: "sha384:d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", algorithm: "sha384", hex: "d3fc7881460b7e22e3d172954463dddd7866d17597e7248453c48b3e9d26d9596bf9c4a9cf8072c9d5bad76e19af801d", }, { // empty hex input: "sha256:", err: ErrDigestInvalidFormat, }, { // just hex input: "d41d8cd98f00b204e9800998ecf8427e", err: ErrDigestInvalidFormat, }, { // not hex input: "sha256:d41d8cd98f00b204e9800m98ecf8427e", err: ErrDigestInvalidFormat, }, { // too short input: "sha256:abcdef0123456789", err: ErrDigestInvalidLength, }, { // too short (from different algorithm) input: "sha512:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", err: ErrDigestInvalidLength, }, { input: "foo:d41d8cd98f00b204e9800998ecf8427e", err: ErrDigestUnsupported, }, } { digest, err := ParseDigest(testcase.input) if err != testcase.err { t.Fatalf("error differed from expected while parsing %q: %v != %v", testcase.input, err, testcase.err) } if testcase.err != nil { continue } if digest.Algorithm() != testcase.algorithm { t.Fatalf("incorrect algorithm for parsed digest: %q != %q", digest.Algorithm(), testcase.algorithm) } if digest.Hex() != testcase.hex { t.Fatalf("incorrect hex for parsed digest: %q != %q", digest.Hex(), testcase.hex) } // Parse string return value and check equality newParsed, err := ParseDigest(digest.String()) if err != nil { t.Fatalf("unexpected error parsing input %q: %v", testcase.input, err) } if newParsed != digest { t.Fatalf("expected equal: %q != %q", newParsed, digest) } } } docker-registry-2.6.2~ds1/digest/digester.go000066400000000000000000000104311313450123100210500ustar00rootroot00000000000000package digest import ( "crypto" "fmt" "hash" "io" ) // Algorithm identifies and implementation of a digester by an identifier. // Note the that this defines both the hash algorithm used and the string // encoding. type Algorithm string // supported digest types const ( SHA256 Algorithm = "sha256" // sha256 with hex encoding SHA384 Algorithm = "sha384" // sha384 with hex encoding SHA512 Algorithm = "sha512" // sha512 with hex encoding // Canonical is the primary digest algorithm used with the distribution // project. Other digests may be used but this one is the primary storage // digest. Canonical = SHA256 ) var ( // TODO(stevvooe): Follow the pattern of the standard crypto package for // registration of digests. Effectively, we are a registerable set and // common symbol access. // algorithms maps values to hash.Hash implementations. Other algorithms // may be available but they cannot be calculated by the digest package. algorithms = map[Algorithm]crypto.Hash{ SHA256: crypto.SHA256, SHA384: crypto.SHA384, SHA512: crypto.SHA512, } ) // Available returns true if the digest type is available for use. If this // returns false, New and Hash will return nil. func (a Algorithm) Available() bool { h, ok := algorithms[a] if !ok { return false } // check availability of the hash, as well return h.Available() } func (a Algorithm) String() string { return string(a) } // Size returns number of bytes returned by the hash. func (a Algorithm) Size() int { h, ok := algorithms[a] if !ok { return 0 } return h.Size() } // Set implemented to allow use of Algorithm as a command line flag. func (a *Algorithm) Set(value string) error { if value == "" { *a = Canonical } else { // just do a type conversion, support is queried with Available. *a = Algorithm(value) } return nil } // New returns a new digester for the specified algorithm. If the algorithm // does not have a digester implementation, nil will be returned. This can be // checked by calling Available before calling New. func (a Algorithm) New() Digester { return &digester{ alg: a, hash: a.Hash(), } } // Hash returns a new hash as used by the algorithm. If not available, the // method will panic. Check Algorithm.Available() before calling. func (a Algorithm) Hash() hash.Hash { if !a.Available() { // NOTE(stevvooe): A missing hash is usually a programming error that // must be resolved at compile time. We don't import in the digest // package to allow users to choose their hash implementation (such as // when using stevvooe/resumable or a hardware accelerated package). // // Applications that may want to resolve the hash at runtime should // call Algorithm.Available before call Algorithm.Hash(). panic(fmt.Sprintf("%v not available (make sure it is imported)", a)) } return algorithms[a].New() } // FromReader returns the digest of the reader using the algorithm. func (a Algorithm) FromReader(rd io.Reader) (Digest, error) { digester := a.New() if _, err := io.Copy(digester.Hash(), rd); err != nil { return "", err } return digester.Digest(), nil } // FromBytes digests the input and returns a Digest. func (a Algorithm) FromBytes(p []byte) Digest { digester := a.New() if _, err := digester.Hash().Write(p); err != nil { // Writes to a Hash should never fail. None of the existing // hash implementations in the stdlib or hashes vendored // here can return errors from Write. Having a panic in this // condition instead of having FromBytes return an error value // avoids unnecessary error handling paths in all callers. panic("write to hash function returned error: " + err.Error()) } return digester.Digest() } // TODO(stevvooe): Allow resolution of verifiers using the digest type and // this registration system. // Digester calculates the digest of written data. Writes should go directly // to the return value of Hash, while calling Digest will return the current // value of the digest. type Digester interface { Hash() hash.Hash // provides direct access to underlying hash instance. Digest() Digest } // digester provides a simple digester definition that embeds a hasher. type digester struct { alg Algorithm hash hash.Hash } func (d *digester) Hash() hash.Hash { return d.hash } func (d *digester) Digest() Digest { return NewDigest(d.alg, d.hash) } docker-registry-2.6.2~ds1/digest/digester_resumable_test.go000066400000000000000000000007771313450123100241620ustar00rootroot00000000000000// +build !noresumabledigest package digest import ( "testing" "github.com/stevvooe/resumable" _ "github.com/stevvooe/resumable/sha256" ) // TestResumableDetection just ensures that the resumable capability of a hash // is exposed through the digester type, which is just a hash plus a Digest // method. func TestResumableDetection(t *testing.T) { d := Canonical.New() if _, ok := d.Hash().(resumable.Hash); !ok { t.Fatalf("expected digester to implement resumable.Hash: %#v, %v", d, d.Hash()) } } docker-registry-2.6.2~ds1/digest/doc.go000066400000000000000000000031401313450123100200060ustar00rootroot00000000000000// Package digest provides a generalized type to opaquely represent message // digests and their operations within the registry. The Digest type is // designed to serve as a flexible identifier in a content-addressable system. // More importantly, it provides tools and wrappers to work with // hash.Hash-based digests with little effort. // // Basics // // The format of a digest is simply a string with two parts, dubbed the // "algorithm" and the "digest", separated by a colon: // // : // // An example of a sha256 digest representation follows: // // sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc // // In this case, the string "sha256" is the algorithm and the hex bytes are // the "digest". // // Because the Digest type is simply a string, once a valid Digest is // obtained, comparisons are cheap, quick and simple to express with the // standard equality operator. // // Verification // // The main benefit of using the Digest type is simple verification against a // given digest. The Verifier interface, modeled after the stdlib hash.Hash // interface, provides a common write sink for digest verification. After // writing is complete, calling the Verifier.Verified method will indicate // whether or not the stream of bytes matches the target digest. // // Missing Features // // In addition to the above, we intend to add the following features to this // package: // // 1. A Digester type that supports write sink digest calculation. // // 2. Suspend and resume of ongoing digest calculations to support efficient digest verification in the registry. // package digest docker-registry-2.6.2~ds1/digest/set.go000066400000000000000000000147221313450123100200440ustar00rootroot00000000000000package digest import ( "errors" "sort" "strings" "sync" ) var ( // ErrDigestNotFound is used when a matching digest // could not be found in a set. ErrDigestNotFound = errors.New("digest not found") // ErrDigestAmbiguous is used when multiple digests // are found in a set. None of the matching digests // should be considered valid matches. ErrDigestAmbiguous = errors.New("ambiguous digest string") ) // Set is used to hold a unique set of digests which // may be easily referenced by easily referenced by a string // representation of the digest as well as short representation. // The uniqueness of the short representation is based on other // digests in the set. If digests are omitted from this set, // collisions in a larger set may not be detected, therefore it // is important to always do short representation lookups on // the complete set of digests. To mitigate collisions, an // appropriately long short code should be used. type Set struct { mutex sync.RWMutex entries digestEntries } // NewSet creates an empty set of digests // which may have digests added. func NewSet() *Set { return &Set{ entries: digestEntries{}, } } // checkShortMatch checks whether two digests match as either whole // values or short values. This function does not test equality, // rather whether the second value could match against the first // value. func checkShortMatch(alg Algorithm, hex, shortAlg, shortHex string) bool { if len(hex) == len(shortHex) { if hex != shortHex { return false } if len(shortAlg) > 0 && string(alg) != shortAlg { return false } } else if !strings.HasPrefix(hex, shortHex) { return false } else if len(shortAlg) > 0 && string(alg) != shortAlg { return false } return true } // Lookup looks for a digest matching the given string representation. // If no digests could be found ErrDigestNotFound will be returned // with an empty digest value. If multiple matches are found // ErrDigestAmbiguous will be returned with an empty digest value. func (dst *Set) Lookup(d string) (Digest, error) { dst.mutex.RLock() defer dst.mutex.RUnlock() if len(dst.entries) == 0 { return "", ErrDigestNotFound } var ( searchFunc func(int) bool alg Algorithm hex string ) dgst, err := ParseDigest(d) if err == ErrDigestInvalidFormat { hex = d searchFunc = func(i int) bool { return dst.entries[i].val >= d } } else { hex = dgst.Hex() alg = dgst.Algorithm() searchFunc = func(i int) bool { if dst.entries[i].val == hex { return dst.entries[i].alg >= alg } return dst.entries[i].val >= hex } } idx := sort.Search(len(dst.entries), searchFunc) if idx == len(dst.entries) || !checkShortMatch(dst.entries[idx].alg, dst.entries[idx].val, string(alg), hex) { return "", ErrDigestNotFound } if dst.entries[idx].alg == alg && dst.entries[idx].val == hex { return dst.entries[idx].digest, nil } if idx+1 < len(dst.entries) && checkShortMatch(dst.entries[idx+1].alg, dst.entries[idx+1].val, string(alg), hex) { return "", ErrDigestAmbiguous } return dst.entries[idx].digest, nil } // Add adds the given digest to the set. An error will be returned // if the given digest is invalid. If the digest already exists in the // set, this operation will be a no-op. func (dst *Set) Add(d Digest) error { if err := d.Validate(); err != nil { return err } dst.mutex.Lock() defer dst.mutex.Unlock() entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d} searchFunc := func(i int) bool { if dst.entries[i].val == entry.val { return dst.entries[i].alg >= entry.alg } return dst.entries[i].val >= entry.val } idx := sort.Search(len(dst.entries), searchFunc) if idx == len(dst.entries) { dst.entries = append(dst.entries, entry) return nil } else if dst.entries[idx].digest == d { return nil } entries := append(dst.entries, nil) copy(entries[idx+1:], entries[idx:len(entries)-1]) entries[idx] = entry dst.entries = entries return nil } // Remove removes the given digest from the set. An err will be // returned if the given digest is invalid. If the digest does // not exist in the set, this operation will be a no-op. func (dst *Set) Remove(d Digest) error { if err := d.Validate(); err != nil { return err } dst.mutex.Lock() defer dst.mutex.Unlock() entry := &digestEntry{alg: d.Algorithm(), val: d.Hex(), digest: d} searchFunc := func(i int) bool { if dst.entries[i].val == entry.val { return dst.entries[i].alg >= entry.alg } return dst.entries[i].val >= entry.val } idx := sort.Search(len(dst.entries), searchFunc) // Not found if idx is after or value at idx is not digest if idx == len(dst.entries) || dst.entries[idx].digest != d { return nil } entries := dst.entries copy(entries[idx:], entries[idx+1:]) entries = entries[:len(entries)-1] dst.entries = entries return nil } // All returns all the digests in the set func (dst *Set) All() []Digest { dst.mutex.RLock() defer dst.mutex.RUnlock() retValues := make([]Digest, len(dst.entries)) for i := range dst.entries { retValues[i] = dst.entries[i].digest } return retValues } // ShortCodeTable returns a map of Digest to unique short codes. The // length represents the minimum value, the maximum length may be the // entire value of digest if uniqueness cannot be achieved without the // full value. This function will attempt to make short codes as short // as possible to be unique. func ShortCodeTable(dst *Set, length int) map[Digest]string { dst.mutex.RLock() defer dst.mutex.RUnlock() m := make(map[Digest]string, len(dst.entries)) l := length resetIdx := 0 for i := 0; i < len(dst.entries); i++ { var short string extended := true for extended { extended = false if len(dst.entries[i].val) <= l { short = dst.entries[i].digest.String() } else { short = dst.entries[i].val[:l] for j := i + 1; j < len(dst.entries); j++ { if checkShortMatch(dst.entries[j].alg, dst.entries[j].val, "", short) { if j > resetIdx { resetIdx = j } extended = true } else { break } } if extended { l++ } } } m[dst.entries[i].digest] = short if i >= resetIdx { l = length } } return m } type digestEntry struct { alg Algorithm val string digest Digest } type digestEntries []*digestEntry func (d digestEntries) Len() int { return len(d) } func (d digestEntries) Less(i, j int) bool { if d[i].val != d[j].val { return d[i].val < d[j].val } return d[i].alg < d[j].alg } func (d digestEntries) Swap(i, j int) { d[i], d[j] = d[j], d[i] } docker-registry-2.6.2~ds1/digest/set_test.go000066400000000000000000000220611313450123100210760ustar00rootroot00000000000000package digest import ( "crypto/sha256" "encoding/binary" "math/rand" "testing" ) func assertEqualDigests(t *testing.T, d1, d2 Digest) { if d1 != d2 { t.Fatalf("Digests do not match:\n\tActual: %s\n\tExpected: %s", d1, d2) } } func TestLookup(t *testing.T) { digests := []Digest{ "sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:5432111111111111111111111111111111111111111111111111111111111111", "sha256:6543111111111111111111111111111111111111111111111111111111111111", "sha256:6432111111111111111111111111111111111111111111111111111111111111", "sha256:6542111111111111111111111111111111111111111111111111111111111111", "sha256:6532111111111111111111111111111111111111111111111111111111111111", } dset := NewSet() for i := range digests { if err := dset.Add(digests[i]); err != nil { t.Fatal(err) } } dgst, err := dset.Lookup("54") if err != nil { t.Fatal(err) } assertEqualDigests(t, dgst, digests[3]) dgst, err = dset.Lookup("1234") if err == nil { t.Fatal("Expected ambiguous error looking up: 1234") } if err != ErrDigestAmbiguous { t.Fatal(err) } dgst, err = dset.Lookup("9876") if err == nil { t.Fatal("Expected ambiguous error looking up: 9876") } if err != ErrDigestNotFound { t.Fatal(err) } dgst, err = dset.Lookup("sha256:1234") if err == nil { t.Fatal("Expected ambiguous error looking up: sha256:1234") } if err != ErrDigestAmbiguous { t.Fatal(err) } dgst, err = dset.Lookup("sha256:12345") if err != nil { t.Fatal(err) } assertEqualDigests(t, dgst, digests[0]) dgst, err = dset.Lookup("sha256:12346") if err != nil { t.Fatal(err) } assertEqualDigests(t, dgst, digests[2]) dgst, err = dset.Lookup("12346") if err != nil { t.Fatal(err) } assertEqualDigests(t, dgst, digests[2]) dgst, err = dset.Lookup("12345") if err != nil { t.Fatal(err) } assertEqualDigests(t, dgst, digests[0]) } func TestAddDuplication(t *testing.T) { digests := []Digest{ "sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:5432111111111111111111111111111111111111111111111111111111111111", "sha256:6543111111111111111111111111111111111111111111111111111111111111", "sha512:65431111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", "sha512:65421111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", "sha512:65321111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", } dset := NewSet() for i := range digests { if err := dset.Add(digests[i]); err != nil { t.Fatal(err) } } if len(dset.entries) != 8 { t.Fatal("Invalid dset size") } if err := dset.Add(Digest("sha256:1234511111111111111111111111111111111111111111111111111111111111")); err != nil { t.Fatal(err) } if len(dset.entries) != 8 { t.Fatal("Duplicate digest insert allowed") } if err := dset.Add(Digest("sha384:123451111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")); err != nil { t.Fatal(err) } if len(dset.entries) != 9 { t.Fatal("Insert with different algorithm not allowed") } } func TestRemove(t *testing.T) { digests, err := createDigests(10) if err != nil { t.Fatal(err) } dset := NewSet() for i := range digests { if err := dset.Add(digests[i]); err != nil { t.Fatal(err) } } dgst, err := dset.Lookup(digests[0].String()) if err != nil { t.Fatal(err) } if dgst != digests[0] { t.Fatalf("Unexpected digest value:\n\tExpected: %s\n\tActual: %s", digests[0], dgst) } if err := dset.Remove(digests[0]); err != nil { t.Fatal(err) } if _, err := dset.Lookup(digests[0].String()); err != ErrDigestNotFound { t.Fatalf("Expected error %v when looking up removed digest, got %v", ErrDigestNotFound, err) } } func TestAll(t *testing.T) { digests, err := createDigests(100) if err != nil { t.Fatal(err) } dset := NewSet() for i := range digests { if err := dset.Add(digests[i]); err != nil { t.Fatal(err) } } all := map[Digest]struct{}{} for _, dgst := range dset.All() { all[dgst] = struct{}{} } if len(all) != len(digests) { t.Fatalf("Unexpected number of unique digests found:\n\tExpected: %d\n\tActual: %d", len(digests), len(all)) } for i, dgst := range digests { if _, ok := all[dgst]; !ok { t.Fatalf("Missing element at position %d: %s", i, dgst) } } } func assertEqualShort(t *testing.T, actual, expected string) { if actual != expected { t.Fatalf("Unexpected short value:\n\tExpected: %s\n\tActual: %s", expected, actual) } } func TestShortCodeTable(t *testing.T) { digests := []Digest{ "sha256:1234111111111111111111111111111111111111111111111111111111111111", "sha256:1234511111111111111111111111111111111111111111111111111111111111", "sha256:1234611111111111111111111111111111111111111111111111111111111111", "sha256:5432111111111111111111111111111111111111111111111111111111111111", "sha256:6543111111111111111111111111111111111111111111111111111111111111", "sha256:6432111111111111111111111111111111111111111111111111111111111111", "sha256:6542111111111111111111111111111111111111111111111111111111111111", "sha256:6532111111111111111111111111111111111111111111111111111111111111", } dset := NewSet() for i := range digests { if err := dset.Add(digests[i]); err != nil { t.Fatal(err) } } dump := ShortCodeTable(dset, 2) if len(dump) < len(digests) { t.Fatalf("Error unexpected size: %d, expecting %d", len(dump), len(digests)) } assertEqualShort(t, dump[digests[0]], "12341") assertEqualShort(t, dump[digests[1]], "12345") assertEqualShort(t, dump[digests[2]], "12346") assertEqualShort(t, dump[digests[3]], "54") assertEqualShort(t, dump[digests[4]], "6543") assertEqualShort(t, dump[digests[5]], "64") assertEqualShort(t, dump[digests[6]], "6542") assertEqualShort(t, dump[digests[7]], "653") } func createDigests(count int) ([]Digest, error) { r := rand.New(rand.NewSource(25823)) digests := make([]Digest, count) for i := range digests { h := sha256.New() if err := binary.Write(h, binary.BigEndian, r.Int63()); err != nil { return nil, err } digests[i] = NewDigest("sha256", h) } return digests, nil } func benchAddNTable(b *testing.B, n int) { digests, err := createDigests(n) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} for j := range digests { if err = dset.Add(digests[j]); err != nil { b.Fatal(err) } } } } func benchLookupNTable(b *testing.B, n int, shortLen int) { digests, err := createDigests(n) if err != nil { b.Fatal(err) } dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} for i := range digests { if err := dset.Add(digests[i]); err != nil { b.Fatal(err) } } shorts := make([]string, 0, n) for _, short := range ShortCodeTable(dset, shortLen) { shorts = append(shorts, short) } b.ResetTimer() for i := 0; i < b.N; i++ { if _, err = dset.Lookup(shorts[i%n]); err != nil { b.Fatal(err) } } } func benchRemoveNTable(b *testing.B, n int) { digests, err := createDigests(n) if err != nil { b.Fatal(err) } b.ResetTimer() for i := 0; i < b.N; i++ { dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} b.StopTimer() for j := range digests { if err = dset.Add(digests[j]); err != nil { b.Fatal(err) } } b.StartTimer() for j := range digests { if err = dset.Remove(digests[j]); err != nil { b.Fatal(err) } } } } func benchShortCodeNTable(b *testing.B, n int, shortLen int) { digests, err := createDigests(n) if err != nil { b.Fatal(err) } dset := &Set{entries: digestEntries(make([]*digestEntry, 0, n))} for i := range digests { if err := dset.Add(digests[i]); err != nil { b.Fatal(err) } } b.ResetTimer() for i := 0; i < b.N; i++ { ShortCodeTable(dset, shortLen) } } func BenchmarkAdd10(b *testing.B) { benchAddNTable(b, 10) } func BenchmarkAdd100(b *testing.B) { benchAddNTable(b, 100) } func BenchmarkAdd1000(b *testing.B) { benchAddNTable(b, 1000) } func BenchmarkRemove10(b *testing.B) { benchRemoveNTable(b, 10) } func BenchmarkRemove100(b *testing.B) { benchRemoveNTable(b, 100) } func BenchmarkRemove1000(b *testing.B) { benchRemoveNTable(b, 1000) } func BenchmarkLookup10(b *testing.B) { benchLookupNTable(b, 10, 12) } func BenchmarkLookup100(b *testing.B) { benchLookupNTable(b, 100, 12) } func BenchmarkLookup1000(b *testing.B) { benchLookupNTable(b, 1000, 12) } func BenchmarkShortCode10(b *testing.B) { benchShortCodeNTable(b, 10, 12) } func BenchmarkShortCode100(b *testing.B) { benchShortCodeNTable(b, 100, 12) } func BenchmarkShortCode1000(b *testing.B) { benchShortCodeNTable(b, 1000, 12) } docker-registry-2.6.2~ds1/digest/verifiers.go000066400000000000000000000017661313450123100212530ustar00rootroot00000000000000package digest import ( "hash" "io" ) // Verifier presents a general verification interface to be used with message // digests and other byte stream verifications. Users instantiate a Verifier // from one of the various methods, write the data under test to it then check // the result with the Verified method. type Verifier interface { io.Writer // Verified will return true if the content written to Verifier matches // the digest. Verified() bool } // NewDigestVerifier returns a verifier that compares the written bytes // against a passed in digest. func NewDigestVerifier(d Digest) (Verifier, error) { if err := d.Validate(); err != nil { return nil, err } return hashVerifier{ hash: d.Algorithm().Hash(), digest: d, }, nil } type hashVerifier struct { digest Digest hash hash.Hash } func (hv hashVerifier) Write(p []byte) (n int, err error) { return hv.hash.Write(p) } func (hv hashVerifier) Verified() bool { return hv.digest == NewDigest(hv.digest.Algorithm(), hv.hash) } docker-registry-2.6.2~ds1/digest/verifiers_test.go000066400000000000000000000020451313450123100223010ustar00rootroot00000000000000package digest import ( "bytes" "crypto/rand" "io" "testing" ) func TestDigestVerifier(t *testing.T) { p := make([]byte, 1<<20) rand.Read(p) digest := FromBytes(p) verifier, err := NewDigestVerifier(digest) if err != nil { t.Fatalf("unexpected error getting digest verifier: %s", err) } io.Copy(verifier, bytes.NewReader(p)) if !verifier.Verified() { t.Fatalf("bytes not verified") } } // TestVerifierUnsupportedDigest ensures that unsupported digest validation is // flowing through verifier creation. func TestVerifierUnsupportedDigest(t *testing.T) { unsupported := Digest("bean:0123456789abcdef") _, err := NewDigestVerifier(unsupported) if err == nil { t.Fatalf("expected error when creating verifier") } if err != ErrDigestUnsupported { t.Fatalf("incorrect error for unsupported digest: %v", err) } } // TODO(stevvooe): Add benchmarks to measure bytes/second throughput for // DigestVerifier. // // The relevant benchmark for comparison can be run with the following // commands: // // go test -bench . crypto/sha1 // docker-registry-2.6.2~ds1/doc.go000066400000000000000000000004661313450123100165370ustar00rootroot00000000000000// Package distribution will define the interfaces for the components of // docker distribution. The goal is to allow users to reliably package, ship // and store content related to docker images. // // This is currently a work in progress. More details are available in the // README.md. package distribution docker-registry-2.6.2~ds1/docs/000077500000000000000000000000001313450123100163655ustar00rootroot00000000000000docker-registry-2.6.2~ds1/docs/README.md000066400000000000000000000012251313450123100176440ustar00rootroot00000000000000# The docs have been moved! The documentation for Registry has been merged into [the general documentation repo](https://github.com/docker/docker.github.io). Commit history has been preserved. The docs for Registry are now here: https://github.com/docker/docker.github.io/tree/master/registry > Note: The definitive [./spec directory](spec/) directory and [configuration.md](configuration.md) file will be maintained in this repository and be refreshed periodically in [the general documentation repo](https://github.com/docker/docker.github.io). As always, the docs in the general repo remain open-source and we appreciate your feedback and pull requests! docker-registry-2.6.2~ds1/docs/architecture.md000066400000000000000000000043521313450123100213750ustar00rootroot00000000000000--- published: false --- # Architecture ## Design **TODO(stevvooe):** Discuss the architecture of the registry, internally and externally, in a few different deployment scenarios. ### Eventual Consistency > **NOTE:** This section belongs somewhere, perhaps in a design document. We > are leaving this here so the information is not lost. Running the registry on eventually consistent backends has been part of the design from the beginning. This section covers some of the approaches to dealing with this reality. There are a few classes of issues that we need to worry about when implementing something on top of the storage drivers: 1. Read-After-Write consistency (see this [article on s3](http://shlomoswidler.com/2009/12/read-after-write-consistency-in-amazon.html)). 2. [Write-Write Conflicts](http://en.wikipedia.org/wiki/Write%E2%80%93write_conflict). In reality, the registry must worry about these kinds of errors when doing the following: 1. Accepting data into a temporary upload file may not have latest data block yet (read-after-write). 2. Moving uploaded data into its blob location (write-write race). 3. Modifying the "current" manifest for given tag (write-write race). 4. A whole slew of operations around deletes (read-after-write, delete-write races, garbage collection, etc.). The backend path layout employs a few techniques to avoid these problems: 1. Large writes are done to private upload directories. This alleviates most of the corruption potential under multiple writers by avoiding multiple writers. 2. Constraints in storage driver implementations, such as support for writing after the end of a file to extend it. 3. Digest verification to avoid data corruption. 4. Manifest files are stored by digest and cannot change. 5. All other non-content files (links, hashes, etc.) are written as an atomic unit. Anything that requires additions and deletions is broken out into separate "files". Last writer still wins. Unfortunately, one must play this game when trying to build something like this on top of eventually consistent storage systems. If we run into serious problems, we can wrap the storagedrivers in a shared consistency layer but that would increase complexity and hinder registry cluster performance. docker-registry-2.6.2~ds1/docs/configuration.md000066400000000000000000001266201313450123100215650ustar00rootroot00000000000000--- title: "Configuring a registry" description: "Explains how to configure a registry" keywords: registry, on-prem, images, tags, repository, distribution, configuration --- The Registry configuration is based on a YAML file, detailed below. While it comes with sane default values out of the box, you should review it exhaustively before moving your systems to production. ## Override specific configuration options In a typical setup where you run your Registry from the official image, you can specify a configuration variable from the environment by passing `-e` arguments to your `docker run` stanza or from within a Dockerfile using the `ENV` instruction. To override a configuration option, create an environment variable named `REGISTRY_variable` where `variable` is the name of the configuration option and the `_` (underscore) represents indention levels. For example, you can configure the `rootdirectory` of the `filesystem` storage backend: ```none storage: filesystem: rootdirectory: /var/lib/registry ``` To override this value, set an environment variable like this: ```none REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/somewhere ``` This variable overrides the `/var/lib/registry` value to the `/somewhere` directory. > **Note**: Create a base configuration file with environment variables that can > be configured to tweak individual values. Overriding configuration sections > with environment variables is not recommended. ## Overriding the entire configuration file If the default configuration is not a sound basis for your usage, or if you are having issues overriding keys from the environment, you can specify an alternate YAML configuration file by mounting it as a volume in the container. Typically, create a new configuration file from scratch,named `config.yml`, then specify it in the `docker run` command: ```bash $ docker run -d -p 5000:5000 --restart=always --name registry \ -v `pwd`/config.yml:/etc/docker/registry/config.yml \ registry:2 ``` Use this [example YAML file](https://github.com/docker/distribution/blob/master/cmd/registry/config-example.yml) as a starting point. ## List of configuration options These are all configuration options for the registry. Some options in the list are mutually exclusive. Read the detailed reference information about each option before finalizing your configuration. ```none version: 0.1 log: accesslog: disabled: true level: debug formatter: text fields: service: registry environment: staging hooks: - type: mail disabled: true levels: - panic options: smtp: addr: mail.example.com:25 username: mailuser password: password insecure: true from: sender@example.com to: - errors@example.com loglevel: debug # deprecated: use "log" storage: filesystem: rootdirectory: /var/lib/registry maxthreads: 100 azure: accountname: accountname accountkey: base64encodedaccountkey container: containername gcs: bucket: bucketname keyfile: /path/to/keyfile rootdirectory: /gcs/object/name/prefix chunksize: 5242880 s3: accesskey: awsaccesskey secretkey: awssecretkey region: us-west-1 regionendpoint: http://myobjects.local bucket: bucketname encrypt: true keyid: mykeyid secure: true v4auth: true chunksize: 5242880 multipartcopychunksize: 33554432 multipartcopymaxconcurrency: 100 multipartcopythresholdsize: 33554432 rootdirectory: /s3/object/name/prefix swift: username: username password: password authurl: https://storage.myprovider.com/auth/v1.0 or https://storage.myprovider.com/v2.0 or https://storage.myprovider.com/v3/auth tenant: tenantname tenantid: tenantid domain: domain name for Openstack Identity v3 API domainid: domain id for Openstack Identity v3 API insecureskipverify: true region: fr container: containername rootdirectory: /swift/object/name/prefix oss: accesskeyid: accesskeyid accesskeysecret: accesskeysecret region: OSS region name endpoint: optional endpoints internal: optional internal endpoint bucket: OSS bucket encrypt: optional data encryption setting secure: optional ssl setting chunksize: optional size valye rootdirectory: optional root directory inmemory: # This driver takes no parameters delete: enabled: false redirect: disable: false cache: blobdescriptor: redis maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false readonly: enabled: false auth: silly: realm: silly-realm service: silly-service token: realm: token-realm service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle htpasswd: realm: basic-realm path: /path/to/htpasswd middleware: registry: - name: ARegistryMiddleware options: foo: bar repository: - name: ARepositoryMiddleware options: foo: bar storage: - name: cloudfront options: baseurl: https://my.cloudfronted.domain.com/ privatekey: /path/to/pem keypairid: cloudfrontkeypairid duration: 3000s storage: - name: redirect options: baseurl: https://example.com/ reporting: bugsnag: apikey: bugsnagapikey releasestage: bugsnagreleasestage endpoint: bugsnagendpoint newrelic: licensekey: newreliclicensekey name: newrelicname verbose: true http: addr: localhost:5000 prefix: /my/nested/registry/ host: https://myregistryaddress.org:5000 secret: asecretforlocaldevelopment relativeurls: false tls: certificate: /path/to/x509/public key: /path/to/x509/private clientcas: - /path/to/ca.pem - /path/to/another/ca.pem letsencrypt: cachefile: /path/to/cache-file email: emailused@letsencrypt.com debug: addr: localhost:5001 headers: X-Content-Type-Options: [nosniff] http2: disabled: false notifications: endpoints: - name: alistener disabled: false url: https://my.listener.com/event headers: timeout: 500 threshold: 5 backoff: 1000 ignoredmediatypes: - application/octet-stream redis: addr: localhost:6379 password: asecret db: 0 dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms pool: maxidle: 16 maxactive: 64 idletimeout: 300s health: storagedriver: enabled: true interval: 10s threshold: 3 file: - file: /path/to/checked/file interval: 10s http: - uri: http://server.to.check/must/return/200 headers: Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==] statuscode: 200 timeout: 3s interval: 10s threshold: 3 tcp: - addr: redis-server.domain.com:6379 timeout: 3s interval: 10s threshold: 3 proxy: remoteurl: https://registry-1.docker.io username: [username] password: [password] compatibility: schema1: signingkeyfile: /etc/registry/key.json validation: enabled: true manifests: urls: allow: - ^https?://([^/]+\.)*example\.com/ deny: - ^https?://www\.example\.com/ ``` In some instances a configuration option is **optional** but it contains child options marked as **required**. In these cases, you can omit the parent with all its children. However, if the parent is included, you must also include all the children marked **required**. ## `version` ```none version: 0.1 ``` The `version` option is **required**. It specifies the configuration's version. It is expected to remain a top-level field, to allow for a consistent version check before parsing the remainder of the configuration file. ## `log` The `log` subsection configures the behavior of the logging system. The logging system outputs everything to stdout. You can adjust the granularity and format with this configuration section. ```none log: accesslog: disabled: true level: debug formatter: text fields: service: registry environment: staging ``` | Parameter | Required | Description | |-------------|----------|-------------| | `level` | no | Sets the sensitivity of logging output. Permitted values are `error`, `warn`, `info`, and `debug`. The default is `info`. | | `formatter` | no | This selects the format of logging output. The format primarily affects how keyed attributes for a log line are encoded. Options are `text`, `json`, and `logstash`. The default is `text`. | | `fields` | no | A map of field names to values. These are added to every log line for the context. This is useful for identifying log messages source after being mixed in other systems. | ### `accesslog` ```none accesslog: disabled: true ``` Within `log`, `accesslog` configures the behavior of the access logging system. By default, the access logging system outputs to stdout in [Combined Log Format](https://httpd.apache.org/docs/2.4/logs.html#combined). Access logging can be disabled by setting the boolean flag `disabled` to `true`. ## `hooks` ```none hooks: - type: mail levels: - panic options: smtp: addr: smtp.sendhost.com:25 username: sendername password: password insecure: true from: name@sendhost.com to: - name@receivehost.com ``` The `hooks` subsection configures the logging hooks' behavior. This subsection includes a sequence handler which you can use for sending mail, for example. Refer to `loglevel` to configure the level of messages printed. ## `loglevel` > **DEPRECATED:** Please use [log](#log) instead. ```none loglevel: debug ``` Permitted values are `error`, `warn`, `info` and `debug`. The default is `info`. ## `storage` ```none storage: filesystem: rootdirectory: /var/lib/registry azure: accountname: accountname accountkey: base64encodedaccountkey container: containername gcs: bucket: bucketname keyfile: /path/to/keyfile rootdirectory: /gcs/object/name/prefix s3: accesskey: awsaccesskey secretkey: awssecretkey region: us-west-1 regionendpoint: http://myobjects.local bucket: bucketname encrypt: true keyid: mykeyid secure: true v4auth: true chunksize: 5242880 multipartcopychunksize: 33554432 multipartcopymaxconcurrency: 100 multipartcopythresholdsize: 33554432 rootdirectory: /s3/object/name/prefix swift: username: username password: password authurl: https://storage.myprovider.com/auth/v1.0 or https://storage.myprovider.com/v2.0 or https://storage.myprovider.com/v3/auth tenant: tenantname tenantid: tenantid domain: domain name for Openstack Identity v3 API domainid: domain id for Openstack Identity v3 API insecureskipverify: true region: fr container: containername rootdirectory: /swift/object/name/prefix oss: accesskeyid: accesskeyid accesskeysecret: accesskeysecret region: OSS region name endpoint: optional endpoints internal: optional internal endpoint bucket: OSS bucket encrypt: optional data encryption setting secure: optional ssl setting chunksize: optional size valye rootdirectory: optional root directory inmemory: delete: enabled: false cache: blobdescriptor: inmemory maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false readonly: enabled: false redirect: disable: false ``` The `storage` option is **required** and defines which storage backend is in use. You must configure exactly one backend. If you configure more, the registry returns an error. You can choose any of these backend storage drivers: | Storage driver | Description | |---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `filesystem` | Uses the local disk to store registry files. It is ideal for development and may be appropriate for some small-scale production applications. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/filesystem.md). | | `azure` | Uses Microsoft Azure Blob Storage. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/azure.md). | | `gcs` | Uses Google Cloud Storage. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/gcs.md). | | `s3` | Uses Amazon Simple Storage Service (S3) and compatible Storage Services. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/s3.md). | | `swift` | Uses Openstack Swift object storage. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/swift.md). | | `oss` | Uses Aliyun OSS for object storage. See the [driver's reference documentation](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/oss.md). | For testing only, you can use the [`inmemory` storage driver](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/inmemory.md). If you would like to run a registry from volatile memory, use the [`filesystem` driver](https://github.com/docker/docker.github.io/tree/master/registry/storage-drivers/filesystem.md) on a ramdisk. If you are deploying a registry on Windows, a Windows volume mounted from the host is not recommended. Instead, you can use a S3 or Azure backing data-store. If you do use a Windows volume, the length of the `PATH` to the mount point must be within the `MAX_PATH` limits (typically 255 characters), or this error will occur: ```none mkdir /XXX protocol error and your registry will not function properly. ``` ### `maintenance` Currently, upload purging and read-only mode are the only `maintenance` functions available. ### `uploadpurging` Upload purging is a background process that periodically removes orphaned files from the upload directories of the registry. Upload purging is enabled by default. To configure upload directory purging, the following parameters must be set. | Parameter | Required | Description | |------------|----------|----------------------------------------------------------------------------------------------------| | `enabled` | yes | Set to `true` to enable upload purging. Defaults to `true`. | | `age` | yes | Upload directories which are older than this age will be deleted.Defaults to `168h` (1 week). | | `interval` | yes | The interval between upload directory purging. Defaults to `24h`. | | `dryrun` | yes | Set `dryrun` to `true` to obtain a summary of what directories will be deleted. Defaults to `false`.| > **Note**: `age` and `interval` are strings containing a number with optional fraction and a unit suffix. Some examples: `45m`, `2h10m`, `168h`. ### `readonly` If the `readonly` section under `maintenance` has `enabled` set to `true`, clients will not be allowed to write to the registry. This mode is useful to temporarily prevent writes to the backend storage so a garbage collection pass can be run. Before running garbage collection, the registry should be restarted with readonly's `enabled` set to true. After the garbage collection pass finishes, the registry may be restarted again, this time with `readonly` removed from the configuration (or set to false). ### `delete` Use the `delete` structure to enable the deletion of image blobs and manifests by digest. It defaults to false, but it can be enabled by writing the following on the configuration file: ```none delete: enabled: true ``` ### `cache` Use the `cache` structure to enable caching of data accessed in the storage backend. Currently, the only available cache provides fast access to layer metadata, which uses the `blobdescriptor` field if configured. You can set `blobdescriptor` field to `redis` or `inmemory`. If set to `redis`,a Redis pool caches layer metadata. If set to `inmemory`, an in-memory map caches layer metadata. > **NOTE**: Formerly, `blobdescriptor` was known as `layerinfo`. While these > are equivalent, `layerinfo` has been deprecated. ### `redirect` The `redirect` subsection provides configuration for managing redirects from content backends. For backends that support it, redirecting is enabled by default. In certain deployment scenarios, you may decide to route all data through the Registry, rather than redirecting to the backend. This may be more efficient when using a backend that is not co-located or when a registry instance is aggressively caching. To disable redirects, add a single flag `disable`, set to `true` under the `redirect` section: ```none redirect: disable: true ``` ## `auth` ```none auth: silly: realm: silly-realm service: silly-service token: realm: token-realm service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle htpasswd: realm: basic-realm path: /path/to/htpasswd ``` The `auth` option is **optional**. Possible auth providers include: - [`silly`](#silly) - [`token`](#token) - [`htpasswd`](#token) You can configure only one authentication provider. ### `silly` The `silly` authentication provider is only appropriate for development. It simply checks for the existence of the `Authorization` header in the HTTP request. It does not check the header's value. If the header does not exist, the `silly` auth responds with a challenge response, echoing back the realm, service, and scope for which access was denied. The following values are used to configure the response: | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `realm` | yes | The realm in which the registry server authenticates. | | `service` | yes | The service being authenticated. | ### `token` Token-based authentication allows you to decouple the authentication system from the registry. It is an established authentication paradigm with a high degree of security. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `realm` | yes | The realm in which the registry server authenticates. | | `service` | yes | The service being authenticated. | | `issuer` | yes | The name of the token issuer. The issuer inserts this into the token so it must match the value configured for the issuer. | | `rootcertbundle` | yes | The absolute path to the root certificate bundle. This bundle contains the public part of the certificates used to sign authentication tokens. | For more information about Token based authentication configuration, see the [specification](spec/auth/token.md). ### `htpasswd` The _htpasswd_ authentication backed allows you to configure basic authentication using an [Apache htpasswd file](https://httpd.apache.org/docs/2.4/programs/htpasswd.html). The only supported password format is [`bcrypt`](http://en.wikipedia.org/wiki/Bcrypt). Entries with other hash types are ignored. The `htpasswd` file is loaded once, at startup. If the file is invalid, the registry will display an error and will not start. > **Warning**: Only use the `htpasswd` authentication scheme with TLS > configured, since basic authentication sends passwords as part of the HTTP > header. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `realm` | yes | The realm in which the registry server authenticates. | | `path` | yes | The path to the `htpasswd` file to load at startup. | ## `middleware` The `middleware` structure is **optional**. Use this option to inject middleware at named hook points. Each middleware must implement the same interface as the object it is wrapping. For instance, a registry middleware must implement the `distribution.Namespace` interface, while a repository middleware must implement `distribution.Repository`, and a storage middleware must implement `driver.StorageDriver`. This is an example configuration of the `cloudfront` middleware, a storage middleware: ```none middleware: registry: - name: ARegistryMiddleware options: foo: bar repository: - name: ARepositoryMiddleware options: foo: bar storage: - name: cloudfront options: baseurl: https://my.cloudfronted.domain.com/ privatekey: /path/to/pem keypairid: cloudfrontkeypairid duration: 3000s ``` Each middleware entry has `name` and `options` entries. The `name` must correspond to the name under which the middleware registers itself. The `options` field is a map that details custom configuration required to initialize the middleware. It is treated as a `map[string]interface{}`. As such, it supports any interesting structures desired, leaving it up to the middleware initialization function to best determine how to handle the specific interpretation of the options. ### `cloudfront` | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `baseurl` | yes | The `SCHEME://HOST[/PATH]` at which Cloudfront is served. | | `privatekey` | yes | The private key for Cloudfront, provided by AWS. | | `keypairid` | yes | The key pair ID provided by AWS. | | `duration` | no | An integer and unit for the duration of the Cloudfront session. Valid time units are `ns`, `us` (or `µs`), `ms`, `s`, `m`, or `h`. For example, `3000s` is valid, but `3000 s` is not. If you do not specify a `duration` or you specify an integer without a time unit, the duration defaults to `20m` (20 minutes).| ### `redirect` You can use the `redirect` storage middleware to specify a custom URL to a location of a proxy for the layer stored by the S3 storage driver. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------------------------------------------------------------| | `baseurl` | yes | `SCHEME://HOST` at which layers are served. Can also contain port. For example, `https://example.com:5443`. | ## `reporting` ``` reporting: bugsnag: apikey: bugsnagapikey releasestage: bugsnagreleasestage endpoint: bugsnagendpoint newrelic: licensekey: newreliclicensekey name: newrelicname verbose: true ``` The `reporting` option is **optional** and configures error and metrics reporting tools. At the moment only two services are supported: - [Bugsnag](#bugsnag) - [New Relic](#new-relic) A valid configuration may contain both. ### `bugsnag` | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `apikey` | yes | The API Key provided by Bugsnag. | | `releasestage` | no | Tracks where the registry is deployed, using a string like `production`, `staging`, or `development`.| | `endpoint`| no | The enterprise Bugsnag endpoint. | ### `newrelic` | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `licensekey` | yes | License key provided by New Relic. | | `name` | no | New Relic application name. | | `verbose`| no | Set to `true` to enable New Relic debugging output on `stdout`. | ## `http` ```none http: addr: localhost:5000 net: tcp prefix: /my/nested/registry/ host: https://myregistryaddress.org:5000 secret: asecretforlocaldevelopment relativeurls: false tls: certificate: /path/to/x509/public key: /path/to/x509/private clientcas: - /path/to/ca.pem - /path/to/another/ca.pem letsencrypt: cachefile: /path/to/cache-file email: emailused@letsencrypt.com debug: addr: localhost:5001 headers: X-Content-Type-Options: [nosniff] http2: disabled: false ``` The `http` option details the configuration for the HTTP server that hosts the registry. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `addr` | yes | The address for which the server should accept connections. The form depends on a network type (see the `net` option). Use `HOST:PORT` for TCP and `FILE` for a UNIX socket. | | `net` | no | The network used to create a listening socket. Known networks are `unix` and `tcp`. | | `prefix` | no | If the server does not run at the root path, set this to the value of the prefix. The root path is the section before `v2`. It requires both preceding and trailing slashes, such as in the example `/path/`. | | `host` | no | A fully-qualified URL for an externally-reachable address for the registry. If present, it is used when creating generated URLs. Otherwise, these URLs are derived from client requests. | | `secret` | no | A random piece of data used to sign state that may be stored with the client to protect against tampering. For production environments you should generate a random piece of data using a cryptographically secure random generator. If you omit the secret, the registry will automatically generate a secret when it starts. **If you are building a cluster of registries behind a load balancer, you MUST ensure the secret is the same for all registries.**| | `relativeurls`| no | If `true`, the registry returns relative URLs in Location headers. The client is responsible for resolving the correct URL. **This option is not compatible with Docker 1.7 and earlier.**| ### `tls` The `tls` structure within `http` is **optional**. Use this to configure TLS for the server. If you already have a web server running on the same host as the registry, you may prefer to configure TLS on that web server and proxy connections to the registry server. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `certificate` | yes | Absolute path to the x509 certificate file. | | `key` | yes | Absolute path to the x509 private key file. | | `clientcas` | no | An array of absolute paths to x509 CA files. | ### `letsencrypt` The `letsencrypt` structure within `tls` is **optional**. Use this to configure TLS certificates provided by [Let's Encrypt](https://letsencrypt.org/how-it-works/). >**NOTE**: When using Let's Encrypt, ensure that the outward-facing address is > accessible on port `443`. The registry defaults to listening on port `5000`. > If you run the registry as a container, consider adding the flag `-p 443:5000` > to the `docker run` command or using a similar setting in a cloud > configuration. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `cachefile` | yes | Absolute path to a file where the Let's Encrypt agent can cache data. | | `email` | yes | The email address used to register with Let's Encrypt. | ### `debug` The `debug` option is **optional** . Use it to configure a debug server that can be helpful in diagnosing problems. The debug endpoint can be used for monitoring registry metrics and health, as well as profiling. Sensitive information may be available via the debug endpoint. Please be certain that access to the debug endpoint is locked down in a production environment. The `debug` section takes a single required `addr` parameter, which specifies the `HOST:PORT` on which the debug server should accept connections. ### `headers` The `headers` option is **optional** . Use it to specify headers that the HTTP server should include in responses. This can be used for security headers such as `Strict-Transport-Security`. The `headers` option should contain an option for each header to include, where the parameter name is the header's name, and the parameter value a list of the header's payload values. Including `X-Content-Type-Options: [nosniff]` is recommended, so that browsers will not interpret content as HTML if they are directed to load a page from the registry. This header is included in the example configuration file. ### `http2` The `http2` structure within `http` is **optional**. Use this to control http2 settings for the registry. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `disabled` | no | If `true`, then `http2` support is disabled. | ## `notifications` ```none notifications: endpoints: - name: alistener disabled: false url: https://my.listener.com/event headers: timeout: 500 threshold: 5 backoff: 1000 ignoredmediatypes: - application/octet-stream ``` The notifications option is **optional** and currently may contain a single option, `endpoints`. ### `endpoints` The `endpoints` structure contains a list of named services (URLs) that can accept event notifications. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `name` | yes | A human-readable name for the service. | | `disabled` | no | If `true`, notifications are disabled for the service.| | `url` | yes | The URL to which events should be published. | | `headers` | yes | A list of static headers to add to each request. Each header's name is a key beneath `headers`, and each value is a list of payloads for that header name. Values must always be lists. | | `timeout` | yes | A value for the HTTP timeout. A positive integer and an optional suffix indicating the unit of time, which may be `ns`, `us`, `ms`, `s`, `m`, or `h`. If you omit the unit of time, `ns` is used. | | `threshold` | yes | An integer specifying how long to wait before backing off a failure. | | `backoff` | yes | How long the system backs off before retrying after a failure. A positive integer and an optional suffix indicating the unit of time, which may be `ns`, `us`, `ms`, `s`, `m`, or `h`. If you omit the unit of time, `ns` is used. | | `ignoredmediatypes`|no| A list of target media types to ignore. Events with these target media types are not published to the endpoint. | ## `redis` ```none redis: addr: localhost:6379 password: asecret db: 0 dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms pool: maxidle: 16 maxactive: 64 idletimeout: 300s ``` Declare parameters for constructing the `redis` connections. Registry instances may use the Redis instance for several applications. Currently, it caches information about immutable blobs. Most of the `redis` options control how the registry connects to the `redis` instance. You can control the pool's behavior with the [pool](#pool) subsection. You should configure Redis with the **allkeys-lru** eviction policy, because the registry does not set an expiration value on keys. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `addr` | yes | The address (host and port) of the Redis instance. | | `password`| no | A password used to authenticate to the Redis instance.| | `db` | no | The name of the database to use for each connection. | | `dialtimeout` | no | The timeout for connecting to the Redis instance. | | `readtimeout` | no | The timeout for reading from the Redis instance. | | `writetimeout` | no | The timeout for writing to the Redis instance. | ### `pool` ```none pool: maxidle: 16 maxactive: 64 idletimeout: 300s ``` Use these settings to configure the behavior of the Redis connection pool. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `maxidle` | no | The maximum number of idle connections in the pool. | | `maxactive`| no | The maximum number of connections which can be open before blocking a connection request. | | `idletimeout`| no | How long to wait before closing inactive connections. | ## `health` ```none health: storagedriver: enabled: true interval: 10s threshold: 3 file: - file: /path/to/checked/file interval: 10s http: - uri: http://server.to.check/must/return/200 headers: Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==] statuscode: 200 timeout: 3s interval: 10s threshold: 3 tcp: - addr: redis-server.domain.com:6379 timeout: 3s interval: 10s threshold: 3 ``` The health option is **optional**, and contains preferences for a periodic health check on the storage driver's backend storage, as well as optional periodic checks on local files, HTTP URIs, and/or TCP servers. The results of the health checks are available at the `/debug/health` endpoint on the debug HTTP server if the debug HTTP server is enabled (see http section). ### `storagedriver` The `storagedriver` structure contains options for a health check on the configured storage driver's backend storage. The health check is only active when `enabled` is set to `true`. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `enabled` | yes | Set to `true` to enable storage driver health checks or `false` to disable them. | | `interval`| no | How long to wait between repetitions of the storage driver health check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. | | `threshold`| no | A positive integer which represents the number of times the check must fail before the state is marked as unhealthy. If not specified, a single failure marks the state as unhealthy. | ### `file` The `file` structure includes a list of paths to be periodically checked for the\ existence of a file. If a file exists at the given path, the health check will fail. You can use this mechanism to bring a registry out of rotation by creating a file. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `file` | yes | The path to check for existence of a file. | | `interval`| no | How long to wait before repeating the check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. | ### `http` The `http` structure includes a list of HTTP URIs to periodically check with `HEAD` requests. If a `HEAD` request does not complete or returns an unexpected status code, the health check will fail. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `uri` | yes | The URI to check. | | `headers` | no | Static headers to add to each request. Each header's name is a key beneath `headers`, and each value is a list of payloads for that header name. Values must always be lists. | | `statuscode` | no | The expected status code from the HTTP URI. Defaults to `200`. | | `timeout` | no | How long to wait before timing out the HTTP request. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. | | `interval`| no | How long to wait before repeating the check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. | | `threshold`| no | The number of times the check must fail before the state is marked as unhealthy. If this field is not specified, a single failure marks the state as unhealthy. | ### `tcp` The `tcp` structure includes a list of TCP addresses to periodically check using TCP connection attempts. Addresses must include port numbers. If a connection attempt fails, the health check will fail. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `addr` | yes | The TCP address and port to connect to. | | `timeout` | no | How long to wait before timing out the TCP connection. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. | | `interval`| no | How long to wait between repetitions of the check. A positive integer and an optional suffix indicating the unit of time. The suffix is one of `ns`, `us`, `ms`, `s`, `m`, or `h`. Defaults to `10s` if the value is omitted. If you specify a value but omit the suffix, the value is interpreted as a number of nanoseconds. | | `threshold`| no | The number of times the check must fail before the state is marked as unhealthy. If this field is not specified, a single failure marks the state as unhealthy. | ## `proxy` ``` proxy: remoteurl: https://registry-1.docker.io username: [username] password: [password] ``` The `proxy` structure allows a registry to be configured as a pull-through cache to Docker Hub. See [mirror](https://github.com/docker/docker.github.io/tree/master/registry/recipes/mirror.md) for more information. Pushing to a registry configured as a pull-through cache is unsupported. | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `remoteurl`| yes | The URL for the repository on Docker Hub. | | `username` | no | The username registered with Docker Hub which has access to the repository. | | `password` | no | The password used to authenticate to Docker Hub using the username specified in `username`. | To enable pulling private repositories (e.g. `batman/robin`) specify the username (such as `batman`) and the password for that username. > **Note**: These private repositories are stored in the proxy cache's storage. > Take appropriate measures to protect access to the proxy cache. ## `compatibility` ```none compatibility: schema1: signingkeyfile: /etc/registry/key.json ``` Use the `compatibility` structure to configure handling of older and deprecated features. Each subsection defines such a feature with configurable behavior. ### `schema1` | Parameter | Required | Description | |-----------|----------|-------------------------------------------------------| | `signingkeyfile` | no | The signing private key used to add signatures to `schema1` manifests. If no signing key is provided, a new ECDSA key is generated when the registry starts. | ## `validation` ```none validation: enabled: true manifests: urls: allow: - ^https?://([^/]+\.)*example\.com/ deny: - ^https?://www\.example\.com/ ``` ### `enabled` Use the `enabled` flag to enable the other options in the `validation` section. They are disabled by default. ### `manifests` Use the `manifest` subsection to configure manifest validation. #### `urls` The `allow` and `deny` options are each a list of [regular expressions](https://godoc.org/regexp/syntax) that restrict the URLs in pushed manifests. If `allow` is unset, pushing a manifest containing URLs fails. If `allow` is set, pushing a manifest succeeds only if all URLs match one of the `allow` regular expressions **and** one of the following holds: 1. `deny` is unset. 2. `deny` is set but no URLs within the manifest match any of the `deny` regular expressions. ## Example: Development configuration You can use this simple example for local development: ```none version: 0.1 log: level: debug storage: filesystem: rootdirectory: /var/lib/registry http: addr: localhost:5000 secret: asecretforlocaldevelopment debug: addr: localhost:5001 ``` This example configures the registry instance to run on port `5000`, binding to `localhost`, with the `debug` server enabled. Registry data is stored in the `/var/lib/registry` directory. Logging is set to `debug` mode, which is the most verbose. See [config-example.yml](https://github.com/docker/distribution/blob/master/cmd/registry/config-example.yml) for another simple configuration. Both examples are generally useful for local development. ## Example: Middleware configuration This example configures [Amazon Cloudfront](http://aws.amazon.com/cloudfront/) as the storage middleware in a registry. Middleware allows the registry to serve layers via a content delivery network (CDN). This reduces requests to the storage layer. Cloudfront requires the S3 storage driver. This is the configuration expressed in YAML: ```none middleware: storage: - name: cloudfront disabled: false options: baseurl: http://d111111abcdef8.cloudfront.net privatekey: /path/to/asecret.pem keypairid: asecret duration: 60 ``` See the configuration reference for [Cloudfront](#cloudfront) for more information about configuration options. > **Note**: Cloudfront keys exist separately from other AWS keys. See > [the documentation on AWS credentials](http://docs.aws.amazon.com/general/latest/gr/aws-security-credentials.html) > for more information. docker-registry-2.6.2~ds1/docs/spec/000077500000000000000000000000001313450123100173175ustar00rootroot00000000000000docker-registry-2.6.2~ds1/docs/spec/api.md000066400000000000000000004363571313450123100204340ustar00rootroot00000000000000--- title: "HTTP API V2" description: "Specification for the Registry API." keywords: ["registry, on-prem, images, tags, repository, distribution, api, advanced"] --- # Docker Registry HTTP API V2 ## Introduction The _Docker Registry HTTP API_ is the protocol to facilitate distribution of images to the docker engine. It interacts with instances of the docker registry, which is a service to manage information about docker images and enable their distribution. The specification covers the operation of version 2 of this API, known as _Docker Registry HTTP API V2_. While the V1 registry protocol is usable, there are several problems with the architecture that have led to this new version. The main driver of this specification is a set of changes to the docker the image format, covered in [docker/docker#8093](https://github.com/docker/docker/issues/8093). The new, self-contained image manifest simplifies image definition and improves security. This specification will build on that work, leveraging new properties of the manifest format to improve performance, reduce bandwidth usage and decrease the likelihood of backend corruption. For relevant details and history leading up to this specification, please see the following issues: - [docker/docker#8093](https://github.com/docker/docker/issues/8093) - [docker/docker#9015](https://github.com/docker/docker/issues/9015) - [docker/docker-registry#612](https://github.com/docker/docker-registry/issues/612) ### Scope This specification covers the URL layout and protocols of the interaction between docker registry and docker core. This will affect the docker core registry API and the rewrite of docker-registry. Docker registry implementations may implement other API endpoints, but they are not covered by this specification. This includes the following features: - Namespace-oriented URI Layout - PUSH/PULL registry server for V2 image manifest format - Resumable layer PUSH support - V2 Client library implementation While authentication and authorization support will influence this specification, details of the protocol will be left to a future specification. Relevant header definitions and error codes are present to provide an indication of what a client may encounter. #### Future There are features that have been discussed during the process of cutting this specification. The following is an incomplete list: - Immutable image references - Multiple architecture support - Migration from v2compatibility representation These may represent features that are either out of the scope of this specification, the purview of another specification or have been deferred to a future version. ### Use Cases For the most part, the use cases of the former registry API apply to the new version. Differentiating use cases are covered below. #### Image Verification A docker engine instance would like to run verified image named "library/ubuntu", with the tag "latest". The engine contacts the registry, requesting the manifest for "library/ubuntu:latest". An untrusted registry returns a manifest. Before proceeding to download the individual layers, the engine verifies the manifest's signature, ensuring that the content was produced from a trusted source and no tampering has occurred. After each layer is downloaded, the engine verifies the digest of the layer, ensuring that the content matches that specified by the manifest. #### Resumable Push Company X's build servers lose connectivity to docker registry before completing an image layer transfer. After connectivity returns, the build server attempts to re-upload the image. The registry notifies the build server that the upload has already been partially attempted. The build server responds by only sending the remaining data to complete the image file. #### Resumable Pull Company X is having more connectivity problems but this time in their deployment datacenter. When downloading an image, the connection is interrupted before completion. The client keeps the partial data and uses http `Range` requests to avoid downloading repeated data. #### Layer Upload De-duplication Company Y's build system creates two identical docker layers from build processes A and B. Build process A completes uploading the layer before B. When process B attempts to upload the layer, the registry indicates that its not necessary because the layer is already known. If process A and B upload the same layer at the same time, both operations will proceed and the first to complete will be stored in the registry (Note: we may modify this to prevent dogpile with some locking mechanism). ### Changes The V2 specification has been written to work as a living document, specifying only what is certain and leaving what is not specified open or to future changes. Only non-conflicting additions should be made to the API and accepted changes should avoid preventing future changes from happening. This section should be updated when changes are made to the specification, indicating what is different. Optionally, we may start marking parts of the specification to correspond with the versions enumerated here. Each set of changes is given a letter corresponding to a set of modifications that were applied to the baseline specification. These are merely for reference and shouldn't be used outside the specification other than to identify a set of modifications.
l
  • Document TOOMANYREQUESTS error code.
k
  • Document use of Accept and Content-Type headers in manifests endpoint.
j
  • Add ability to mount blobs across repositories.
i
  • Clarified expected behavior response to manifest HEAD request.
h
  • All mention of tarsum removed.
g
  • Clarify behavior of pagination behavior with unspecified parameters.
f
  • Specify the delete API for layers and manifests.
e
  • Added support for listing registry contents.
  • Added pagination to tags API.
  • Added common approach to support pagination.
d
  • Allow repository name components to be one character.
  • Clarified that single component names are allowed.
c
  • Added section covering digest format.
  • Added more clarification that manifest cannot be deleted by tag.
b
  • Added capability of doing streaming upload to PATCH blob upload.
  • Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.
  • Removed `416 Requested Range Not Satisfiable` response status from PUT blob upload.
a
  • Added support for immutable manifest references in manifest endpoints.
  • Deleting a manifest by tag has been deprecated.
  • Specified `Docker-Content-Digest` header for appropriate entities.
  • Added error code for unsupported operations.
## Overview This section covers client flows and details of the API endpoints. The URI layout of the new API is structured to support a rich authentication and authorization model by leveraging namespaces. All endpoints will be prefixed by the API version and the repository name: /v2// For example, an API endpoint that will work with the `library/ubuntu` repository, the URI prefix will be: /v2/library/ubuntu/ This scheme provides rich access control over various operations and methods using the URI prefix and http methods that can be controlled in variety of ways. Classically, repository names have always been two path components where each path component is less than 30 characters. The V2 registry API does not enforce this. The rules for a repository name are as follows: 1. A repository name is broken up into _path components_. A component of a repository name must be at least one lowercase, alpha-numeric characters, optionally separated by periods, dashes or underscores. More strictly, it must match the regular expression `[a-z0-9]+(?:[._-][a-z0-9]+)*`. 2. If a repository name has two or more path components, they must be separated by a forward slash ("/"). 3. The total length of a repository name, including slashes, must be less than 256 characters. These name requirements _only_ apply to the registry API and should accept a superset of what is supported by other docker ecosystem components. All endpoints should support aggressive http caching, compression and range headers, where appropriate. The new API attempts to leverage HTTP semantics where possible but may break from standards to implement targeted features. For detail on individual endpoints, please see the [_Detail_](#detail) section. ### Errors Actionable failure conditions, covered in detail in their relevant sections, are reported as part of 4xx responses, in a json response body. One or more errors will be returned in the following format: { "errors:" [{ "code": , "message": , "detail": }, ... ] } The `code` field will be a unique identifier, all caps with underscores by convention. The `message` field will be a human readable string. The optional `detail` field may contain arbitrary json data providing information the client can use to resolve the issue. While the client can take action on certain error codes, the registry may add new error codes over time. All client implementations should treat unknown error codes as `UNKNOWN`, allowing future error codes to be added without breaking API compatibility. For the purposes of the specification error codes will only be added and never removed. For a complete account of all error codes, please see the [_Errors_](#errors-2) section. ### API Version Check A minimal endpoint, mounted at `/v2/` will provide version support information based on its response statuses. The request format is as follows: GET /v2/ If a `200 OK` response is returned, the registry implements the V2(.1) registry API and the client may proceed safely with other V2 operations. Optionally, the response may contain information about the supported paths in the response body. The client should be prepared to ignore this data. If a `401 Unauthorized` response is returned, the client should take action based on the contents of the "WWW-Authenticate" header and try the endpoint again. Depending on access control setup, the client may still have to authenticate against different resources, even if this check succeeds. If `404 Not Found` response status, or other unexpected status, is returned, the client should proceed with the assumption that the registry does not implement V2 of the API. When a `200 OK` or `401 Unauthorized` response is returned, the "Docker-Distribution-API-Version" header should be set to "registry/2.0". Clients may require this header value to determine if the endpoint serves this API. When this header is omitted, clients may fallback to an older API version. ### Content Digests This API design is driven heavily by [content addressability](http://en.wikipedia.org/wiki/Content-addressable_storage). The core of this design is the concept of a content addressable identifier. It uniquely identifies content by taking a collision-resistant hash of the bytes. Such an identifier can be independently calculated and verified by selection of a common _algorithm_. If such an identifier can be communicated in a secure manner, one can retrieve the content from an insecure source, calculate it independently and be certain that the correct content was obtained. Put simply, the identifier is a property of the content. To disambiguate from other concepts, we call this identifier a _digest_. A _digest_ is a serialized hash result, consisting of a _algorithm_ and _hex_ portion. The _algorithm_ identifies the methodology used to calculate the digest. The _hex_ portion is the hex-encoded result of the hash. We define a _digest_ string to match the following grammar: ``` digest := algorithm ":" hex algorithm := /[A-Fa-f0-9_+.-]+/ hex := /[A-Fa-f0-9]+/ ``` Some examples of _digests_ include the following: digest | description | ----------------------------------------------------------------------------------|------------------------------------------------ sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b | Common sha256 based digest | While the _algorithm_ does allow one to implement a wide variety of algorithms, compliant implementations should use sha256. Heavy processing of input before calculating a hash is discouraged to avoid degrading the uniqueness of the _digest_ but some canonicalization may be performed to ensure consistent identifiers. Let's use a simple example in pseudo-code to demonstrate a digest calculation: ``` let C = 'a small string' let B = sha256(C) let D = 'sha256:' + EncodeHex(B) let ID(C) = D ``` Above, we have bytestring `C` passed into a function, `SHA256`, that returns a bytestring `B`, which is the hash of `C`. `D` gets the algorithm concatenated with the hex encoding of `B`. We then define the identifier of `C` to `ID(C)` as equal to `D`. A digest can be verified by independently calculating `D` and comparing it with identifier `ID(C)`. #### Digest Header To provide verification of http content, any response may include a `Docker-Content-Digest` header. This will include the digest of the target entity returned in the response. For blobs, this is the entire blob content. For manifests, this is the manifest body without the signature content, also known as the JWS payload. Note that the commonly used canonicalization for digest calculation may be dependent on the mediatype of the content, such as with manifests. The client may choose to ignore the header or may verify it to ensure content integrity and transport security. This is most important when fetching by a digest. To ensure security, the content should be verified against the digest used to fetch the content. At times, the returned digest may differ from that used to initiate a request. Such digests are considered to be from different _domains_, meaning they have different values for _algorithm_. In such a case, the client may choose to verify the digests in both domains or ignore the server's digest. To maintain security, the client _must_ always verify the content against the _digest_ used to fetch the content. > __IMPORTANT:__ If a _digest_ is used to fetch content, the client should use > the same digest used to fetch the content to verify it. The header > `Docker-Content-Digest` should not be trusted over the "local" digest. ### Pulling An Image An "image" is a combination of a JSON manifest and individual layer files. The process of pulling an image centers around retrieving these two components. The first step in pulling an image is to retrieve the manifest. For reference, the relevant manifest fields for the registry are the following: field | description | ----------|------------------------------------------------| name | The name of the image. | tag | The tag for this version of the image. | fsLayers | A list of layer descriptors (including digest) | signature | A JWS used to verify the manifest content | For more information about the manifest format, please see [docker/docker#8093](https://github.com/docker/docker/issues/8093). When the manifest is in hand, the client must verify the signature to ensure the names and layers are valid. Once confirmed, the client will then use the digests to download the individual layers. Layers are stored in as blobs in the V2 registry API, keyed by their digest. #### Pulling an Image Manifest The image manifest can be fetched with the following url: ``` GET /v2//manifests/ ``` The `name` and `reference` parameter identify the image and are required. The reference may include a tag or digest. The client should include an Accept header indicating which manifest content types it supports. For more details on the manifest formats and their content types, see [manifest-v2-1.md](manifest-v2-1.md) and [manifest-v2-2.md](manifest-v2-2.md). In a successful response, the Content-Type header will indicate which manifest type is being returned. A `404 Not Found` response will be returned if the image is unknown to the registry. If the image exists and the response is successful, the image manifest will be returned, with the following format (see [docker/docker#8093](https://github.com/docker/docker/issues/8093) for details): { "name": , "tag": , "fsLayers": [ { "blobSum": }, ... ] ], "history": , "signature": } The client should verify the returned manifest signature for authenticity before fetching layers. ##### Existing Manifests The image manifest can be checked for existence with the following url: ``` HEAD /v2//manifests/ ``` The `name` and `reference` parameter identify the image and are required. The reference may include a tag or digest. A `404 Not Found` response will be returned if the image is unknown to the registry. If the image exists and the response is successful the response will be as follows: ``` 200 OK Content-Length: Docker-Content-Digest: ``` #### Pulling a Layer Layers are stored in the blob portion of the registry, keyed by digest. Pulling a layer is carried out by a standard http request. The URL is as follows: GET /v2//blobs/ Access to a layer will be gated by the `name` of the repository but is identified uniquely in the registry by `digest`. This endpoint may issue a 307 (302 for /blobs/uploads/ ``` The parameters of this request are the image namespace under which the layer will be linked. Responses to this request are covered below. ##### Existing Layers The existence of a layer can be checked via a `HEAD` request to the blob store API. The request should be formatted as follows: ``` HEAD /v2//blobs/ ``` If the layer with the digest specified in `digest` is available, a 200 OK response will be received, with no actual body content (this is according to http specification). The response will look as follows: ``` 200 OK Content-Length: Docker-Content-Digest: ``` When this response is received, the client can assume that the layer is already available in the registry under the given name and should take no further action to upload the layer. Note that the binary digests may differ for the existing registry layer, but the digests will be guaranteed to match. ##### Uploading the Layer If the POST request is successful, a `202 Accepted` response will be returned with the upload URL in the `Location` header: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` The rest of the upload process can be carried out with the returned url, called the "Upload URL" from the `Location` header. All responses to the upload url, whether sending data or getting status, will be in this format. Though the URI format (`/v2//blobs/uploads/`) for the `Location` header is specified, clients should treat it as an opaque url and should never try to assemble it. While the `uuid` parameter may be an actual UUID, this proposal imposes no constraints on the format and clients should never impose any. If clients need to correlate local upload state with remote upload state, the contents of the `Docker-Upload-UUID` header should be used. Such an id can be used to key the last used location header when implementing resumable uploads. ##### Upload Progress The progress and chunk coordination of the upload process will be coordinated through the `Range` header. While this is a non-standard use of the `Range` header, there are examples of [similar approaches](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol) in APIs with heavy use. For an upload that just started, for an example with a 1000 byte layer file, the `Range` header would be as follows: ``` Range: bytes=0-0 ``` To get the status of an upload, issue a GET request to the upload URL: ``` GET /v2//blobs/uploads/ Host: ``` The response will be similar to the above, except will return 204 status: ``` 204 No Content Location: /v2//blobs/uploads/ Range: bytes=0- Docker-Upload-UUID: ``` Note that the HTTP `Range` header byte ranges are inclusive and that will be honored, even in non-standard use cases. ##### Monolithic Upload A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking. To carry out a "monolithic" upload, one can simply put the entire content blob to the provided URL: ``` PUT /v2//blobs/uploads/?digest= Content-Length: Content-Type: application/octet-stream ``` The "digest" parameter must be included with the PUT request. Please see the [_Completed Upload_](#completed-upload) section for details on the parameters and expected responses. ##### Chunked Upload To carry out an upload of a chunk, the client can specify a range header and only include that part of the layer file: ``` PATCH /v2//blobs/uploads/ Content-Length: Content-Range: - Content-Type: application/octet-stream ``` There is no enforcement on layer chunk splits other than that the server must receive them in order. The server may enforce a minimum chunk size. If the server cannot accept the chunk, a `416 Requested Range Not Satisfiable` response will be returned and will include a `Range` header indicating the current status: ``` 416 Requested Range Not Satisfiable Location: /v2//blobs/uploads/ Range: 0- Content-Length: 0 Docker-Upload-UUID: ``` If this response is received, the client should resume from the "last valid range" and upload the subsequent chunk. A 416 will be returned under the following conditions: - Invalid Content-Range header format - Out of order chunk: the range of the next chunk must start immediately after the "last valid range" from the previous response. When a chunk is accepted as part of the upload, a `202 Accepted` response will be returned, including a `Range` header with the current upload status: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` ##### Completed Upload For an upload to be considered complete, the client must submit a `PUT` request on the upload endpoint with a digest parameter. If it is not provided, the upload will not be considered complete. The format for the final chunk will be as follows: ``` PUT /v2//blob/uploads/?digest= Content-Length: Content-Range: - Content-Type: application/octet-stream ``` Optionally, if all chunks have already been uploaded, a `PUT` request with a `digest` parameter and zero-length body may be sent to complete and validated the upload. Multiple "digest" parameters may be provided with different digests. The server may verify none or all of them but _must_ notify the client if the content is rejected. When the last chunk is received and the layer has been validated, the client will receive a `201 Created` response: ``` 201 Created Location: /v2//blobs/ Content-Length: 0 Docker-Content-Digest: ``` The `Location` header will contain the registry URL to access the accepted layer file. The `Docker-Content-Digest` header returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. ###### Digest Parameter The "digest" parameter is designed as an opaque parameter to support verification of a successful transfer. For example, an HTTP URI parameter might be as follows: ``` sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b ``` Given this parameter, the registry will verify that the provided content does match this digest. ##### Canceling an Upload An upload can be cancelled by issuing a DELETE request to the upload endpoint. The format will be as follows: ``` DELETE /v2//blobs/uploads/ ``` After this request is issued, the upload uuid will no longer be valid and the registry server will dump all intermediate data. While uploads will time out if not completed, clients should issue this request if they encounter a fatal error but still have the ability to issue an http request. ##### Cross Repository Blob Mount A blob may be mounted from another repository that the client has read access to, removing the need to upload a blob already known to the registry. To issue a blob mount instead of an upload, a POST request should be issued in the following format: ``` POST /v2//blobs/uploads/?mount=&from= Content-Length: 0 ``` If the blob is successfully mounted, the client will receive a `201 Created` response: ``` 201 Created Location: /v2//blobs/ Content-Length: 0 Docker-Content-Digest: ``` The `Location` header will contain the registry URL to access the accepted layer file. The `Docker-Content-Digest` header returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior and return a `202 Accepted` with the upload URL in the `Location` header: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` This behavior is consistent with older versions of the registry, which do not recognize the repository mount query parameters. Note: a client may issue a HEAD request to check existence of a blob in a source repository to distinguish between the registry not supporting blob mounts and the blob not existing in the expected repository. ##### Errors If an 502, 503 or 504 error is received, the client should assume that the download can proceed due to a temporary condition, honoring the appropriate retry mechanism. Other 5xx errors should be treated as terminal. If there is a problem with the upload, a 4xx error will be returned indicating the problem. After receiving a 4xx response (except 416, as called out above), the upload will be considered failed and the client should take appropriate action. Note that the upload url will not be available forever. If the upload uuid is unknown to the registry, a `404 Not Found` response will be returned and the client must restart the upload process. ### Deleting a Layer A layer may be deleted from the registry via its `name` and `digest`. A delete may be issued with the following request format: DELETE /v2//blobs/ If the blob exists and has been successfully deleted, the following response will be issued: 202 Accepted Content-Length: None If the blob had already been deleted or did not exist, a `404 Not Found` response will be issued instead. If a layer is deleted which is referenced by a manifest in the registry, then the complete images will not be resolvable. #### Pushing an Image Manifest Once all of the layers for an image are uploaded, the client can upload the image manifest. An image can be pushed using the following request format: PUT /v2//manifests/ Content-Type: { "name": , "tag": , "fsLayers": [ { "blobSum": }, ... ] ], "history": , "signature": , ... } The `name` and `reference` fields of the response body must match those specified in the URL. The `reference` field may be a "tag" or a "digest". The content type should match the type of the manifest being uploaded, as specified in [manifest-v2-1.md](manifest-v2-1.md) and [manifest-v2-2.md](manifest-v2-2.md). If there is a problem with pushing the manifest, a relevant 4xx response will be returned with a JSON error message. Please see the [_PUT Manifest_](#put-manifest) section for details on possible error codes that may be returned. If one or more layers are unknown to the registry, `BLOB_UNKNOWN` errors are returned. The `detail` field of the error response will have a `digest` field identifying the missing blob. An error is returned for each unknown blob. The response format is as follows: { "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": } }, ... ] } ### Listing Repositories Images are stored in collections, known as a _repository_, which is keyed by a `name`, as seen throughout the API specification. A registry instance may contain several repositories. The list of available repositories is made available through the _catalog_. The catalog for a given registry can be retrieved with the following request: ``` GET /v2/_catalog ``` The response will be in the following format: ``` 200 OK Content-Type: application/json { "repositories": [ , ... ] } ``` Note that the contents of the response are specific to the registry implementation. Some registries may opt to provide a full catalog output, limit it based on the user's access level or omit upstream results, if providing mirroring functionality. Subsequently, the presence of a repository in the catalog listing only means that the registry *may* provide access to the repository at the time of the request. Conversely, a missing entry does *not* mean that the registry does not have the repository. More succinctly, the presence of a repository only guarantees that it is there but not that it is _not_ there. For registries with a large number of repositories, this response may be quite large. If such a response is expected, one should use pagination. A registry may also limit the amount of responses returned even if pagination was not explicitly requested. In this case the `Link` header will be returned along with the results, and subsequent results can be obtained by following the link as if pagination had been initially requested. For details of the `Link` header, please see the [_Pagination_](#pagination) section. #### Pagination Paginated catalog results can be retrieved by adding an `n` parameter to the request URL, declaring that the response should be limited to `n` results. Starting a paginated flow begins as follows: ``` GET /v2/_catalog?n= ``` The above specifies that a catalog response should be returned, from the start of the result set, ordered lexically, limiting the number of results to `n`. The response to such a request would look as follows: ``` 200 OK Content-Type: application/json Link: <?n=&last=>; rel="next" { "repositories": [ , ... ] } ``` The above includes the _first_ `n` entries from the result set. To get the _next_ `n` entries, one can create a URL where the argument `last` has the value from `repositories[len(repositories)-1]`. If there are indeed more results, the URL for the next block is encoded in an [RFC5988](https://tools.ietf.org/html/rfc5988) `Link` header, as a "next" relation. The presence of the `Link` header communicates to the client that the entire result set has not been returned and another request must be issued. If the header is not present, the client can assume that all results have been received. > __NOTE:__ In the request template above, note that the brackets > are required. For example, if the url is > `http://example.com/v2/_catalog?n=20&last=b`, the value of the header would > be `; rel="next"`. Please see > [RFC5988](https://tools.ietf.org/html/rfc5988) for details. Compliant client implementations should always use the `Link` header value when proceeding through results linearly. The client may construct URLs to skip forward in the catalog. To get the next result set, a client would issue the request as follows, using the URL encoded in the described `Link` header: ``` GET /v2/_catalog?n=&last= ``` The above process should then be repeated until the `Link` header is no longer set. The catalog result set is represented abstractly as a lexically sorted list, where the position in that list can be specified by the query term `last`. The entries in the response start _after_ the term specified by `last`, up to `n` entries. The behavior of `last` is quite simple when demonstrated with an example. Let us say the registry has the following repositories: ``` a b c d ``` If the value of `n` is 2, _a_ and _b_ will be returned on the first response. The `Link` header returned on the response will have `n` set to 2 and last set to _b_: ``` Link: <?n=2&last=b>; rel="next" ``` The client can then issue the request with the above value from the `Link` header, receiving the values _c_ and _d_. Note that `n` may change on the second to last response or be fully omitted, depending on the server implementation. ### Listing Image Tags It may be necessary to list all of the tags under a given repository. The tags for an image repository can be retrieved with the following request: GET /v2//tags/list The response will be in the following format: 200 OK Content-Type: application/json { "name": , "tags": [ , ... ] } For repositories with a large number of tags, this response may be quite large. If such a response is expected, one should use the pagination. #### Pagination Paginated tag results can be retrieved by adding the appropriate parameters to the request URL described above. The behavior of tag pagination is identical to that specified for catalog pagination. We cover a simple flow to highlight any differences. Starting a paginated flow may begin as follows: ``` GET /v2//tags/list?n= ``` The above specifies that a tags response should be returned, from the start of the result set, ordered lexically, limiting the number of results to `n`. The response to such a request would look as follows: ``` 200 OK Content-Type: application/json Link: <?n=&last=>; rel="next" { "name": , "tags": [ , ... ] } ``` To get the next result set, a client would issue the request as follows, using the value encoded in the [RFC5988](https://tools.ietf.org/html/rfc5988) `Link` header: ``` GET /v2//tags/list?n=&last= ``` The above process should then be repeated until the `Link` header is no longer set in the response. The behavior of the `last` parameter, the provided response result, lexical ordering and encoding of the `Link` header are identical to that of catalog pagination. ### Deleting an Image An image may be deleted from the registry via its `name` and `reference`. A delete may be issued with the following request format: DELETE /v2//manifests/ For deletes, `reference` *must* be a digest or the delete will fail. If the image exists and has been successfully deleted, the following response will be issued: 202 Accepted Content-Length: None If the image had already been deleted or did not exist, a `404 Not Found` response will be issued instead. > **Note** When deleting a manifest from a registry version 2.3 or later, the > following header must be used when `HEAD` or `GET`-ing the manifest to obtain > the correct digest to delete: Accept: application/vnd.docker.distribution.manifest.v2+json > for more details, see: [compatibility.md](../compatibility.md#content-addressable-storage-cas) ## Detail > **Note**: This section is still under construction. For the purposes of > implementation, if any details below differ from the described request flows > above, the section below should be corrected. When they match, this note > should be removed. The behavior of the endpoints are covered in detail in this section, organized by route and entity. All aspects of the request and responses are covered, including headers, parameters and body formats. Examples of requests and their corresponding responses, with success and failure, are enumerated. > **Note**: The sections on endpoint detail are arranged with an example > request, a description of the request, followed by information about that > request. A list of methods and URIs are covered in the table below: |Method|Path|Entity|Description| |------|----|------|-----------| | GET | `/v2/` | Base | Check that the endpoint implements Docker Registry API V2. | | GET | `/v2//tags/list` | Tags | Fetch the tags under the repository identified by `name`. | | GET | `/v2//manifests/` | Manifest | Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. | | PUT | `/v2//manifests/` | Manifest | Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest. | | DELETE | `/v2//manifests/` | Manifest | Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`. | | GET | `/v2//blobs/` | Blob | Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. | | DELETE | `/v2//blobs/` | Blob | Delete the blob identified by `name` and `digest` | | POST | `/v2//blobs/uploads/` | Initiate Blob Upload | Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request. | | GET | `/v2//blobs/uploads/` | Blob Upload | Retrieve status of upload identified by `uuid`. The primary purpose of this endpoint is to resolve the current status of a resumable upload. | | PATCH | `/v2//blobs/uploads/` | Blob Upload | Upload a chunk of data for the specified upload. | | PUT | `/v2//blobs/uploads/` | Blob Upload | Complete the upload specified by `uuid`, optionally appending the body as the final chunk. | | DELETE | `/v2//blobs/uploads/` | Blob Upload | Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads will eventually timeout. | | GET | `/v2/_catalog` | Catalog | Retrieve a sorted, json list of repositories available in the registry. | The detail for each endpoint is covered in the following sections. ### Errors The error codes encountered via the API are enumerated in the following table: |Code|Message|Description| |----|-------|-----------| `BLOB_UNKNOWN` | blob unknown to registry | 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. `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. `DIGEST_INVALID` | provided digest did not match uploaded content | 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. `MANIFEST_BLOB_UNKNOWN` | blob unknown to registry | This error may be returned when a manifest blob is unknown to the registry. `MANIFEST_INVALID` | manifest invalid | 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. `MANIFEST_UNKNOWN` | manifest unknown | This error is returned when the manifest, identified by name and tag is unknown to the repository. `MANIFEST_UNVERIFIED` | manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned. `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. `SIZE_INVALID` | provided length did not match content length | 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. `TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. ### Base Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authentication. #### GET Base Check that the endpoint implements Docker Registry API V2. ``` GET /v2/ Host: Authorization: ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| ###### On Success: OK ``` 200 OK ``` The API implements V2 protocol and is accessible. ###### On Failure: Not Found ``` 404 Not Found ``` The registry does not implement the V2 API. ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ### Tags Retrieve information about tags. #### GET Tags Fetch the tags under the repository identified by `name`. ##### Tags ``` GET /v2//tags/list Host: Authorization: ``` Return all tags for the repository The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| ###### On Success: OK ``` 200 OK Content-Length: Content-Type: application/json; charset=utf-8 { "name": , "tags": [ , ... ] } ``` A list of tags for the named repository. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ##### Tags Paginated ``` GET /v2//tags/list?n=&last= ``` Return a portion of the tags for the specified repository. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`name`|path|Name of the target repository.| |`n`|query|Limit the number of entries in each response. It not present, all entries will be returned.| |`last`|query|Result set will include values lexically after last.| ###### On Success: OK ``` 200 OK Content-Length: Link: <?n=&last=>; rel="next" Content-Type: application/json; charset=utf-8 { "name": , "tags": [ , ... ], } ``` A list of tags for the named repository. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| |`Link`|RFC5988 compliant rel='next' with URL to next result set, if available| ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ### Manifest Create, update, delete and retrieve manifests. #### GET Manifest Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. ``` GET /v2//manifests/ Host: Authorization: ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`reference`|path|Tag or digest of the target manifest.| ###### On Success: OK ``` 200 OK Docker-Content-Digest: Content-Type: { "name": , "tag": , "fsLayers": [ { "blobSum": "" }, ... ] ], "history": , "signature": } ``` The manifest identified by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The name or reference was invalid. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | #### PUT Manifest Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest. ``` PUT /v2//manifests/ Host: Authorization: Content-Type: { "name": , "tag": , "fsLayers": [ { "blobSum": "" }, ... ] ], "history": , "signature": } ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`reference`|path|Tag or digest of the target manifest.| ###### On Success: Created ``` 201 Created Location: Content-Length: 0 Docker-Content-Digest: ``` The manifest has been accepted by the registry and is stored under the specified `name` and `tag`. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|The canonical location url of the uploaded manifest.| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Failure: Invalid Manifest ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The received manifest was invalid in some way, as described by the error codes. The client should resolve the issue and retry the request. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. | | `MANIFEST_INVALID` | manifest invalid | 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. | | `MANIFEST_UNVERIFIED` | manifest failed signature verification | During manifest upload, if the manifest fails signature verification, this error will be returned. | | `BLOB_UNKNOWN` | blob unknown to registry | 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. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ###### On Failure: Missing Layer(s) ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": "" } }, ... ] } ``` One or more layers may be missing during a manifest upload. If so, the missing layers will be enumerated in the error response. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UNKNOWN` | blob unknown to registry | 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. | ###### On Failure: Not allowed ``` 405 Method Not Allowed ``` Manifest put is not allowed because the registry is configured as a pull-through cache or for some other reason The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | #### DELETE Manifest Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`. ``` DELETE /v2//manifests/ Host: Authorization: ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`reference`|path|Tag or digest of the target manifest.| ###### On Success: Accepted ``` 202 Accepted ``` ###### On Failure: Invalid Name or Reference ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The specified `name` or `reference` were invalid and the delete was unable to proceed. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `TAG_INVALID` | manifest tag did not match URI | During a manifest upload, if the tag in the manifest does not match the uri tag, this error will be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ###### On Failure: Unknown Manifest ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | | `MANIFEST_UNKNOWN` | manifest unknown | This error is returned when the manifest, identified by name and tag is unknown to the repository. | ###### On Failure: Not allowed ``` 405 Method Not Allowed ``` Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | ### Blob Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest. #### GET Blob Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data. ##### Fetch Blob ``` GET /v2//blobs/ Host: Authorization: ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`digest`|path|Digest of desired blob.| ###### On Success: OK ``` 200 OK Content-Length: Docker-Content-Digest: Content-Type: application/octet-stream ``` The blob identified by `digest` is available. The blob content will be present in the body of the request. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|The length of the requested blob content.| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Success: Temporary Redirect ``` 307 Temporary Redirect Location: Docker-Content-Digest: ``` The blob identified by `digest` is available at the provided location. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|The location where the layer should be accessible.| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The blob, identified by `name` and `digest`, is unknown to the registry. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | | `BLOB_UNKNOWN` | blob unknown to registry | 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. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ##### Fetch Blob Part ``` GET /v2//blobs/ Host: Authorization: Range: bytes=- ``` This endpoint may also support RFC7233 compliant range requests. Support can be detected by issuing a HEAD request. If the header `Accept-Range: bytes` is returned, range requests can be used to fetch partial content. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Range`|header|HTTP Range header specifying blob chunk.| |`name`|path|Name of the target repository.| |`digest`|path|Digest of desired blob.| ###### On Success: Partial Content ``` 206 Partial Content Content-Length: Content-Range: bytes -/ Content-Type: application/octet-stream ``` The blob identified by `digest` is available. The specified chunk of blob content will be present in the body of the request. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|The length of the requested blob chunk.| |`Content-Range`|Content range of blob chunk.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | | `BLOB_UNKNOWN` | blob unknown to registry | 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. | ###### On Failure: Requested Range Not Satisfiable ``` 416 Requested Range Not Satisfiable ``` The range specification cannot be satisfied for the requested content. This can happen when the range is not formatted correctly or if the range is outside of the valid size of the content. ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | #### DELETE Blob Delete the blob identified by `name` and `digest` ``` DELETE /v2//blobs/ Host: Authorization: ``` The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`digest`|path|Digest of desired blob.| ###### On Success: Accepted ``` 202 Accepted Content-Length: 0 Docker-Content-Digest: ``` The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|0| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Failure: Invalid Name or Digest ``` 400 Bad Request ``` The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The blob, identified by `name` and `digest`, is unknown to the registry. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | | `BLOB_UNKNOWN` | blob unknown to registry | 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. | ###### On Failure: Method Not Allowed ``` 405 Method Not Allowed Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` Blob delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ### Initiate Blob Upload Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads. #### POST Initiate Blob Upload Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request. ##### Initiate Monolithic Blob Upload ``` POST /v2//blobs/uploads/?digest= Host: Authorization: Content-Length: Content-Type: application/octect-stream ``` Upload a blob identified by the `digest` parameter in single request. This upload will not be resumable unless a recoverable error is returned. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Length`|header|| |`name`|path|Name of the target repository.| |`digest`|query|Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.| ###### On Success: Created ``` 201 Created Location: Content-Length: 0 Docker-Upload-UUID: ``` The blob has been created in the registry and is available at the provided location. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Invalid Name or Digest ``` 400 Bad Request ``` The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | ###### On Failure: Not allowed ``` 405 Method Not Allowed ``` Blob upload is not allowed because the registry is configured as a pull-through cache or for some other reason The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ##### Initiate Resumable Blob Upload ``` POST /v2//blobs/uploads/ Host: Authorization: Content-Length: 0 ``` Initiate a resumable blob upload with an empty request body. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Length`|header|The `Content-Length` header must be zero and the body must be empty.| |`name`|path|Name of the target repository.| ###### On Success: Accepted ``` 202 Accepted Content-Length: 0 Location: /v2//blobs/uploads/ Range: 0-0 Docker-Upload-UUID: ``` The upload has been created. The `Location` header must be used to complete the upload. The response should be identical to a `GET` request on the contents of the returned `Location` header. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Location`|The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required.| |`Range`|Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Invalid Name or Digest ``` 400 Bad Request ``` The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ##### Mount Blob ``` POST /v2//blobs/uploads/?mount=&from= Host: Authorization: Content-Length: 0 ``` Mount a blob identified by the `mount` parameter from another repository. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Length`|header|The `Content-Length` header must be zero and the body must be empty.| |`name`|path|Name of the target repository.| |`mount`|query|Digest of blob to mount from the source repository.| |`from`|query|Name of the source repository.| ###### On Success: Created ``` 201 Created Location: Content-Length: 0 Docker-Upload-UUID: ``` The blob has been mounted in the repository and is available at the provided location. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Invalid Name or Digest ``` 400 Bad Request ``` The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | ###### On Failure: Not allowed ``` 405 Method Not Allowed ``` Blob mount is not allowed because the registry is configured as a pull-through cache or for some other reason The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ### Blob Upload Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls. #### GET Blob Upload Retrieve status of upload identified by `uuid`. The primary purpose of this endpoint is to resolve the current status of a resumable upload. ``` GET /v2//blobs/uploads/ Host: Authorization: ``` Retrieve the progress of the current upload, as reported by the `Range` header. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.| ###### On Success: Upload Progress ``` 204 No Content Range: 0- Content-Length: 0 Docker-Upload-UUID: ``` The upload is known and in progress. The last received offset is available in the `Range` header. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Range`|Range indicating the current progress of the upload.| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was an error processing the upload and it must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The upload is unknown to the registry. The upload must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | #### PATCH Blob Upload Upload a chunk of data for the specified upload. ##### Stream upload ``` PATCH /v2//blobs/uploads/ Host: Authorization: Content-Type: application/octet-stream ``` Upload a stream of data to upload without completing the upload. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`name`|path|Name of the target repository.| |`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.| ###### On Success: Data Accepted ``` 204 No Content Location: /v2//blobs/uploads/ Range: 0- Content-Length: 0 Docker-Upload-UUID: ``` The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.| |`Range`|Range indicating the current progress of the upload.| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was an error processing the upload and it must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The upload is unknown to the registry. The upload must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ##### Chunked upload ``` PATCH /v2//blobs/uploads/ Host: Authorization: Content-Range: - Content-Length: Content-Type: application/octet-stream ``` Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Range`|header|Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header.| |`Content-Length`|header|Length of the chunk being uploaded, corresponding the length of the request body.| |`name`|path|Name of the target repository.| |`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.| ###### On Success: Chunk Accepted ``` 204 No Content Location: /v2//blobs/uploads/ Range: 0- Content-Length: 0 Docker-Upload-UUID: ``` The chunk of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.| |`Range`|Range indicating the current progress of the upload.| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Upload-UUID`|Identifies the docker upload uuid for the current request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was an error processing the upload and it must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The upload is unknown to the registry. The upload must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. | ###### On Failure: Requested Range Not Satisfiable ``` 416 Requested Range Not Satisfiable ``` The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid. ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | #### PUT Blob Upload Complete the upload specified by `uuid`, optionally appending the body as the final chunk. ``` PUT /v2//blobs/uploads/?digest= Host: Authorization: Content-Length: Content-Type: application/octet-stream ``` Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Length`|header|Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.| |`name`|path|Name of the target repository.| |`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.| |`digest`|query|Digest of uploaded blob.| ###### On Success: Upload Complete ``` 204 No Content Location: Content-Range: - Content-Length: 0 Docker-Content-Digest: ``` The upload has been completed and accepted by the registry. The canonical location will be available in the `Location` header. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Location`|The canonical location of the blob for retrieval| |`Content-Range`|Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| |`Docker-Content-Digest`|Digest of the targeted content for the request.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` There was an error processing the upload and it must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DIGEST_INVALID` | provided digest did not match uploaded content | 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. | | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. | | `UNSUPPORTED` | The operation is unsupported. | The operation was unsupported due to a missing implementation or invalid set of parameters. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The upload is unknown to the registry. The upload must be restarted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | #### DELETE Blob Upload Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads will eventually timeout. ``` DELETE /v2//blobs/uploads/ Host: Authorization: Content-Length: 0 ``` Cancel the upload specified by `uuid`. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`Host`|header|Standard HTTP Host Header. Should be set to the registry host.| |`Authorization`|header|An RFC7235 compliant authorization header.| |`Content-Length`|header|The `Content-Length` header must be zero and the body must be empty.| |`name`|path|Name of the target repository.| |`uuid`|path|A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.| ###### On Success: Upload Deleted ``` 204 No Content Content-Length: 0 ``` The upload has been successfully deleted. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|The `Content-Length` header must be zero and the body must be empty.| ###### On Failure: Bad Request ``` 400 Bad Request Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` An error was encountered processing the delete. The client may ignore this error. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_INVALID` | invalid repository name | Invalid repository name encountered either during manifest validation or any API operation. | | `BLOB_UPLOAD_INVALID` | blob upload invalid | The blob upload encountered an error and can no longer proceed. | ###### On Failure: Not Found ``` 404 Not Found Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The upload is unknown to the registry. The client may ignore this error and assume the upload has been deleted. The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `BLOB_UPLOAD_UNKNOWN` | blob upload unknown to registry | If a blob upload has been cancelled or was never started, this error code may be returned. | ###### On Failure: Authentication Required ``` 401 Unauthorized WWW-Authenticate: realm="", ..." Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client is not authenticated. The following headers will be returned on the response: |Name|Description| |----|-----------| |`WWW-Authenticate`|An RFC7235 compliant authentication challenge header.| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `UNAUTHORIZED` | authentication required | The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate. | ###### On Failure: No Such Repository Error ``` 404 Not Found Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The repository is not known to the registry. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `NAME_UNKNOWN` | repository name not known to registry | This is returned if the name used during an operation is unknown to the registry. | ###### On Failure: Access Denied ``` 403 Forbidden Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client does not have required access to the repository. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `DENIED` | requested access to the resource is denied | The access controller denied access for the operation on a resource. | ###### On Failure: Too Many Requests ``` 429 Too Many Requests Content-Length: Content-Type: application/json; charset=utf-8 { "errors:" [ { "code": , "message": "", "detail": ... }, ... ] } ``` The client made too many requests within a time interval. The following headers will be returned on the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| | `TOOMANYREQUESTS` | too many requests | Returned when a client attempts to contact a service too many times | ### Catalog List a set of available repositories in the local registry cluster. Does not provide any indication of what may be available upstream. Applications can only determine if a repository is available but not if it is not available. #### GET Catalog Retrieve a sorted, json list of repositories available in the registry. ##### Catalog Fetch ``` GET /v2/_catalog ``` Request an unabridged list of repositories available. The implementation may impose a maximum limit and return a partial set with pagination links. ###### On Success: OK ``` 200 OK Content-Length: Content-Type: application/json; charset=utf-8 { "repositories": [ , ... ] } ``` Returns the unabridged list of repositories as a json response. The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| ##### Catalog Fetch Paginated ``` GET /v2/_catalog?n=&last= ``` Return the specified portion of repositories. The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| |`n`|query|Limit the number of entries in each response. It not present, all entries will be returned.| |`last`|query|Result set will include values lexically after last.| ###### On Success: OK ``` 200 OK Content-Length: Link: <?n=&last=>; rel="next" Content-Type: application/json; charset=utf-8 { "repositories": [ , ... ] "next": "?last=&n=" } ``` The following headers will be returned with the response: |Name|Description| |----|-----------| |`Content-Length`|Length of the JSON response body.| |`Link`|RFC5988 compliant rel='next' with URL to next result set, if available| docker-registry-2.6.2~ds1/docs/spec/api.md.tmpl000066400000000000000000001234631313450123100213760ustar00rootroot00000000000000--- title: "HTTP API V2" description: "Specification for the Registry API." keywords: ["registry, on-prem, images, tags, repository, distribution, api, advanced"] --- # Docker Registry HTTP API V2 ## Introduction The _Docker Registry HTTP API_ is the protocol to facilitate distribution of images to the docker engine. It interacts with instances of the docker registry, which is a service to manage information about docker images and enable their distribution. The specification covers the operation of version 2 of this API, known as _Docker Registry HTTP API V2_. While the V1 registry protocol is usable, there are several problems with the architecture that have led to this new version. The main driver of this specification is a set of changes to the docker the image format, covered in [docker/docker#8093](https://github.com/docker/docker/issues/8093). The new, self-contained image manifest simplifies image definition and improves security. This specification will build on that work, leveraging new properties of the manifest format to improve performance, reduce bandwidth usage and decrease the likelihood of backend corruption. For relevant details and history leading up to this specification, please see the following issues: - [docker/docker#8093](https://github.com/docker/docker/issues/8093) - [docker/docker#9015](https://github.com/docker/docker/issues/9015) - [docker/docker-registry#612](https://github.com/docker/docker-registry/issues/612) ### Scope This specification covers the URL layout and protocols of the interaction between docker registry and docker core. This will affect the docker core registry API and the rewrite of docker-registry. Docker registry implementations may implement other API endpoints, but they are not covered by this specification. This includes the following features: - Namespace-oriented URI Layout - PUSH/PULL registry server for V2 image manifest format - Resumable layer PUSH support - V2 Client library implementation While authentication and authorization support will influence this specification, details of the protocol will be left to a future specification. Relevant header definitions and error codes are present to provide an indication of what a client may encounter. #### Future There are features that have been discussed during the process of cutting this specification. The following is an incomplete list: - Immutable image references - Multiple architecture support - Migration from v2compatibility representation These may represent features that are either out of the scope of this specification, the purview of another specification or have been deferred to a future version. ### Use Cases For the most part, the use cases of the former registry API apply to the new version. Differentiating use cases are covered below. #### Image Verification A docker engine instance would like to run verified image named "library/ubuntu", with the tag "latest". The engine contacts the registry, requesting the manifest for "library/ubuntu:latest". An untrusted registry returns a manifest. Before proceeding to download the individual layers, the engine verifies the manifest's signature, ensuring that the content was produced from a trusted source and no tampering has occurred. After each layer is downloaded, the engine verifies the digest of the layer, ensuring that the content matches that specified by the manifest. #### Resumable Push Company X's build servers lose connectivity to docker registry before completing an image layer transfer. After connectivity returns, the build server attempts to re-upload the image. The registry notifies the build server that the upload has already been partially attempted. The build server responds by only sending the remaining data to complete the image file. #### Resumable Pull Company X is having more connectivity problems but this time in their deployment datacenter. When downloading an image, the connection is interrupted before completion. The client keeps the partial data and uses http `Range` requests to avoid downloading repeated data. #### Layer Upload De-duplication Company Y's build system creates two identical docker layers from build processes A and B. Build process A completes uploading the layer before B. When process B attempts to upload the layer, the registry indicates that its not necessary because the layer is already known. If process A and B upload the same layer at the same time, both operations will proceed and the first to complete will be stored in the registry (Note: we may modify this to prevent dogpile with some locking mechanism). ### Changes The V2 specification has been written to work as a living document, specifying only what is certain and leaving what is not specified open or to future changes. Only non-conflicting additions should be made to the API and accepted changes should avoid preventing future changes from happening. This section should be updated when changes are made to the specification, indicating what is different. Optionally, we may start marking parts of the specification to correspond with the versions enumerated here. Each set of changes is given a letter corresponding to a set of modifications that were applied to the baseline specification. These are merely for reference and shouldn't be used outside the specification other than to identify a set of modifications.
l
  • Document TOOMANYREQUESTS error code.
k
  • Document use of Accept and Content-Type headers in manifests endpoint.
j
  • Add ability to mount blobs across repositories.
i
  • Clarified expected behavior response to manifest HEAD request.
h
  • All mention of tarsum removed.
g
  • Clarify behavior of pagination behavior with unspecified parameters.
f
  • Specify the delete API for layers and manifests.
e
  • Added support for listing registry contents.
  • Added pagination to tags API.
  • Added common approach to support pagination.
d
  • Allow repository name components to be one character.
  • Clarified that single component names are allowed.
c
  • Added section covering digest format.
  • Added more clarification that manifest cannot be deleted by tag.
b
  • Added capability of doing streaming upload to PATCH blob upload.
  • Updated PUT blob upload to no longer take final chunk, now requires entire data or no data.
  • Removed `416 Requested Range Not Satisfiable` response status from PUT blob upload.
a
  • Added support for immutable manifest references in manifest endpoints.
  • Deleting a manifest by tag has been deprecated.
  • Specified `Docker-Content-Digest` header for appropriate entities.
  • Added error code for unsupported operations.
## Overview This section covers client flows and details of the API endpoints. The URI layout of the new API is structured to support a rich authentication and authorization model by leveraging namespaces. All endpoints will be prefixed by the API version and the repository name: /v2// For example, an API endpoint that will work with the `library/ubuntu` repository, the URI prefix will be: /v2/library/ubuntu/ This scheme provides rich access control over various operations and methods using the URI prefix and http methods that can be controlled in variety of ways. Classically, repository names have always been two path components where each path component is less than 30 characters. The V2 registry API does not enforce this. The rules for a repository name are as follows: 1. A repository name is broken up into _path components_. A component of a repository name must be at least one lowercase, alpha-numeric characters, optionally separated by periods, dashes or underscores. More strictly, it must match the regular expression `[a-z0-9]+(?:[._-][a-z0-9]+)*`. 2. If a repository name has two or more path components, they must be separated by a forward slash ("/"). 3. The total length of a repository name, including slashes, must be less than 256 characters. These name requirements _only_ apply to the registry API and should accept a superset of what is supported by other docker ecosystem components. All endpoints should support aggressive http caching, compression and range headers, where appropriate. The new API attempts to leverage HTTP semantics where possible but may break from standards to implement targeted features. For detail on individual endpoints, please see the [_Detail_](#detail) section. ### Errors Actionable failure conditions, covered in detail in their relevant sections, are reported as part of 4xx responses, in a json response body. One or more errors will be returned in the following format: { "errors:" [{ "code": , "message": , "detail": }, ... ] } The `code` field will be a unique identifier, all caps with underscores by convention. The `message` field will be a human readable string. The optional `detail` field may contain arbitrary json data providing information the client can use to resolve the issue. While the client can take action on certain error codes, the registry may add new error codes over time. All client implementations should treat unknown error codes as `UNKNOWN`, allowing future error codes to be added without breaking API compatibility. For the purposes of the specification error codes will only be added and never removed. For a complete account of all error codes, please see the [_Errors_](#errors-2) section. ### API Version Check A minimal endpoint, mounted at `/v2/` will provide version support information based on its response statuses. The request format is as follows: GET /v2/ If a `200 OK` response is returned, the registry implements the V2(.1) registry API and the client may proceed safely with other V2 operations. Optionally, the response may contain information about the supported paths in the response body. The client should be prepared to ignore this data. If a `401 Unauthorized` response is returned, the client should take action based on the contents of the "WWW-Authenticate" header and try the endpoint again. Depending on access control setup, the client may still have to authenticate against different resources, even if this check succeeds. If `404 Not Found` response status, or other unexpected status, is returned, the client should proceed with the assumption that the registry does not implement V2 of the API. When a `200 OK` or `401 Unauthorized` response is returned, the "Docker-Distribution-API-Version" header should be set to "registry/2.0". Clients may require this header value to determine if the endpoint serves this API. When this header is omitted, clients may fallback to an older API version. ### Content Digests This API design is driven heavily by [content addressability](http://en.wikipedia.org/wiki/Content-addressable_storage). The core of this design is the concept of a content addressable identifier. It uniquely identifies content by taking a collision-resistant hash of the bytes. Such an identifier can be independently calculated and verified by selection of a common _algorithm_. If such an identifier can be communicated in a secure manner, one can retrieve the content from an insecure source, calculate it independently and be certain that the correct content was obtained. Put simply, the identifier is a property of the content. To disambiguate from other concepts, we call this identifier a _digest_. A _digest_ is a serialized hash result, consisting of a _algorithm_ and _hex_ portion. The _algorithm_ identifies the methodology used to calculate the digest. The _hex_ portion is the hex-encoded result of the hash. We define a _digest_ string to match the following grammar: ``` digest := algorithm ":" hex algorithm := /[A-Fa-f0-9_+.-]+/ hex := /[A-Fa-f0-9]+/ ``` Some examples of _digests_ include the following: digest | description | ----------------------------------------------------------------------------------|------------------------------------------------ sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b | Common sha256 based digest | While the _algorithm_ does allow one to implement a wide variety of algorithms, compliant implementations should use sha256. Heavy processing of input before calculating a hash is discouraged to avoid degrading the uniqueness of the _digest_ but some canonicalization may be performed to ensure consistent identifiers. Let's use a simple example in pseudo-code to demonstrate a digest calculation: ``` let C = 'a small string' let B = sha256(C) let D = 'sha256:' + EncodeHex(B) let ID(C) = D ``` Above, we have bytestring `C` passed into a function, `SHA256`, that returns a bytestring `B`, which is the hash of `C`. `D` gets the algorithm concatenated with the hex encoding of `B`. We then define the identifier of `C` to `ID(C)` as equal to `D`. A digest can be verified by independently calculating `D` and comparing it with identifier `ID(C)`. #### Digest Header To provide verification of http content, any response may include a `Docker-Content-Digest` header. This will include the digest of the target entity returned in the response. For blobs, this is the entire blob content. For manifests, this is the manifest body without the signature content, also known as the JWS payload. Note that the commonly used canonicalization for digest calculation may be dependent on the mediatype of the content, such as with manifests. The client may choose to ignore the header or may verify it to ensure content integrity and transport security. This is most important when fetching by a digest. To ensure security, the content should be verified against the digest used to fetch the content. At times, the returned digest may differ from that used to initiate a request. Such digests are considered to be from different _domains_, meaning they have different values for _algorithm_. In such a case, the client may choose to verify the digests in both domains or ignore the server's digest. To maintain security, the client _must_ always verify the content against the _digest_ used to fetch the content. > __IMPORTANT:__ If a _digest_ is used to fetch content, the client should use > the same digest used to fetch the content to verify it. The header > `Docker-Content-Digest` should not be trusted over the "local" digest. ### Pulling An Image An "image" is a combination of a JSON manifest and individual layer files. The process of pulling an image centers around retrieving these two components. The first step in pulling an image is to retrieve the manifest. For reference, the relevant manifest fields for the registry are the following: field | description | ----------|------------------------------------------------| name | The name of the image. | tag | The tag for this version of the image. | fsLayers | A list of layer descriptors (including digest) | signature | A JWS used to verify the manifest content | For more information about the manifest format, please see [docker/docker#8093](https://github.com/docker/docker/issues/8093). When the manifest is in hand, the client must verify the signature to ensure the names and layers are valid. Once confirmed, the client will then use the digests to download the individual layers. Layers are stored in as blobs in the V2 registry API, keyed by their digest. #### Pulling an Image Manifest The image manifest can be fetched with the following url: ``` GET /v2//manifests/ ``` The `name` and `reference` parameter identify the image and are required. The reference may include a tag or digest. The client should include an Accept header indicating which manifest content types it supports. For more details on the manifest formats and their content types, see [manifest-v2-1.md](manifest-v2-1.md) and [manifest-v2-2.md](manifest-v2-2.md). In a successful response, the Content-Type header will indicate which manifest type is being returned. A `404 Not Found` response will be returned if the image is unknown to the registry. If the image exists and the response is successful, the image manifest will be returned, with the following format (see [docker/docker#8093](https://github.com/docker/docker/issues/8093) for details): { "name": , "tag": , "fsLayers": [ { "blobSum": }, ... ] ], "history": , "signature": } The client should verify the returned manifest signature for authenticity before fetching layers. ##### Existing Manifests The image manifest can be checked for existence with the following url: ``` HEAD /v2//manifests/ ``` The `name` and `reference` parameter identify the image and are required. The reference may include a tag or digest. A `404 Not Found` response will be returned if the image is unknown to the registry. If the image exists and the response is successful the response will be as follows: ``` 200 OK Content-Length: Docker-Content-Digest: ``` #### Pulling a Layer Layers are stored in the blob portion of the registry, keyed by digest. Pulling a layer is carried out by a standard http request. The URL is as follows: GET /v2//blobs/ Access to a layer will be gated by the `name` of the repository but is identified uniquely in the registry by `digest`. This endpoint may issue a 307 (302 for /blobs/uploads/ ``` The parameters of this request are the image namespace under which the layer will be linked. Responses to this request are covered below. ##### Existing Layers The existence of a layer can be checked via a `HEAD` request to the blob store API. The request should be formatted as follows: ``` HEAD /v2//blobs/ ``` If the layer with the digest specified in `digest` is available, a 200 OK response will be received, with no actual body content (this is according to http specification). The response will look as follows: ``` 200 OK Content-Length: Docker-Content-Digest: ``` When this response is received, the client can assume that the layer is already available in the registry under the given name and should take no further action to upload the layer. Note that the binary digests may differ for the existing registry layer, but the digests will be guaranteed to match. ##### Uploading the Layer If the POST request is successful, a `202 Accepted` response will be returned with the upload URL in the `Location` header: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` The rest of the upload process can be carried out with the returned url, called the "Upload URL" from the `Location` header. All responses to the upload url, whether sending data or getting status, will be in this format. Though the URI format (`/v2//blobs/uploads/`) for the `Location` header is specified, clients should treat it as an opaque url and should never try to assemble it. While the `uuid` parameter may be an actual UUID, this proposal imposes no constraints on the format and clients should never impose any. If clients need to correlate local upload state with remote upload state, the contents of the `Docker-Upload-UUID` header should be used. Such an id can be used to key the last used location header when implementing resumable uploads. ##### Upload Progress The progress and chunk coordination of the upload process will be coordinated through the `Range` header. While this is a non-standard use of the `Range` header, there are examples of [similar approaches](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol) in APIs with heavy use. For an upload that just started, for an example with a 1000 byte layer file, the `Range` header would be as follows: ``` Range: bytes=0-0 ``` To get the status of an upload, issue a GET request to the upload URL: ``` GET /v2//blobs/uploads/ Host: ``` The response will be similar to the above, except will return 204 status: ``` 204 No Content Location: /v2//blobs/uploads/ Range: bytes=0- Docker-Upload-UUID: ``` Note that the HTTP `Range` header byte ranges are inclusive and that will be honored, even in non-standard use cases. ##### Monolithic Upload A monolithic upload is simply a chunked upload with a single chunk and may be favored by clients that would like to avoided the complexity of chunking. To carry out a "monolithic" upload, one can simply put the entire content blob to the provided URL: ``` PUT /v2//blobs/uploads/?digest= Content-Length: Content-Type: application/octet-stream ``` The "digest" parameter must be included with the PUT request. Please see the [_Completed Upload_](#completed-upload) section for details on the parameters and expected responses. ##### Chunked Upload To carry out an upload of a chunk, the client can specify a range header and only include that part of the layer file: ``` PATCH /v2//blobs/uploads/ Content-Length: Content-Range: - Content-Type: application/octet-stream ``` There is no enforcement on layer chunk splits other than that the server must receive them in order. The server may enforce a minimum chunk size. If the server cannot accept the chunk, a `416 Requested Range Not Satisfiable` response will be returned and will include a `Range` header indicating the current status: ``` 416 Requested Range Not Satisfiable Location: /v2//blobs/uploads/ Range: 0- Content-Length: 0 Docker-Upload-UUID: ``` If this response is received, the client should resume from the "last valid range" and upload the subsequent chunk. A 416 will be returned under the following conditions: - Invalid Content-Range header format - Out of order chunk: the range of the next chunk must start immediately after the "last valid range" from the previous response. When a chunk is accepted as part of the upload, a `202 Accepted` response will be returned, including a `Range` header with the current upload status: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` ##### Completed Upload For an upload to be considered complete, the client must submit a `PUT` request on the upload endpoint with a digest parameter. If it is not provided, the upload will not be considered complete. The format for the final chunk will be as follows: ``` PUT /v2//blob/uploads/?digest= Content-Length: Content-Range: - Content-Type: application/octet-stream ``` Optionally, if all chunks have already been uploaded, a `PUT` request with a `digest` parameter and zero-length body may be sent to complete and validated the upload. Multiple "digest" parameters may be provided with different digests. The server may verify none or all of them but _must_ notify the client if the content is rejected. When the last chunk is received and the layer has been validated, the client will receive a `201 Created` response: ``` 201 Created Location: /v2//blobs/ Content-Length: 0 Docker-Content-Digest: ``` The `Location` header will contain the registry URL to access the accepted layer file. The `Docker-Content-Digest` header returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. ###### Digest Parameter The "digest" parameter is designed as an opaque parameter to support verification of a successful transfer. For example, an HTTP URI parameter might be as follows: ``` sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b ``` Given this parameter, the registry will verify that the provided content does match this digest. ##### Canceling an Upload An upload can be cancelled by issuing a DELETE request to the upload endpoint. The format will be as follows: ``` DELETE /v2//blobs/uploads/ ``` After this request is issued, the upload uuid will no longer be valid and the registry server will dump all intermediate data. While uploads will time out if not completed, clients should issue this request if they encounter a fatal error but still have the ability to issue an http request. ##### Cross Repository Blob Mount A blob may be mounted from another repository that the client has read access to, removing the need to upload a blob already known to the registry. To issue a blob mount instead of an upload, a POST request should be issued in the following format: ``` POST /v2//blobs/uploads/?mount=&from= Content-Length: 0 ``` If the blob is successfully mounted, the client will receive a `201 Created` response: ``` 201 Created Location: /v2//blobs/ Content-Length: 0 Docker-Content-Digest: ``` The `Location` header will contain the registry URL to access the accepted layer file. The `Docker-Content-Digest` header returns the canonical digest of the uploaded blob which may differ from the provided digest. Most clients may ignore the value but if it is used, the client should verify the value against the uploaded blob data. If a mount fails due to invalid repository or digest arguments, the registry will fall back to the standard upload behavior and return a `202 Accepted` with the upload URL in the `Location` header: ``` 202 Accepted Location: /v2//blobs/uploads/ Range: bytes=0- Content-Length: 0 Docker-Upload-UUID: ``` This behavior is consistent with older versions of the registry, which do not recognize the repository mount query parameters. Note: a client may issue a HEAD request to check existence of a blob in a source repository to distinguish between the registry not supporting blob mounts and the blob not existing in the expected repository. ##### Errors If an 502, 503 or 504 error is received, the client should assume that the download can proceed due to a temporary condition, honoring the appropriate retry mechanism. Other 5xx errors should be treated as terminal. If there is a problem with the upload, a 4xx error will be returned indicating the problem. After receiving a 4xx response (except 416, as called out above), the upload will be considered failed and the client should take appropriate action. Note that the upload url will not be available forever. If the upload uuid is unknown to the registry, a `404 Not Found` response will be returned and the client must restart the upload process. ### Deleting a Layer A layer may be deleted from the registry via its `name` and `digest`. A delete may be issued with the following request format: DELETE /v2//blobs/ If the blob exists and has been successfully deleted, the following response will be issued: 202 Accepted Content-Length: None If the blob had already been deleted or did not exist, a `404 Not Found` response will be issued instead. If a layer is deleted which is referenced by a manifest in the registry, then the complete images will not be resolvable. #### Pushing an Image Manifest Once all of the layers for an image are uploaded, the client can upload the image manifest. An image can be pushed using the following request format: PUT /v2//manifests/ Content-Type: { "name": , "tag": , "fsLayers": [ { "blobSum": }, ... ] ], "history": , "signature": , ... } The `name` and `reference` fields of the response body must match those specified in the URL. The `reference` field may be a "tag" or a "digest". The content type should match the type of the manifest being uploaded, as specified in [manifest-v2-1.md](manifest-v2-1.md) and [manifest-v2-2.md](manifest-v2-2.md). If there is a problem with pushing the manifest, a relevant 4xx response will be returned with a JSON error message. Please see the [_PUT Manifest_](#put-manifest) section for details on possible error codes that may be returned. If one or more layers are unknown to the registry, `BLOB_UNKNOWN` errors are returned. The `detail` field of the error response will have a `digest` field identifying the missing blob. An error is returned for each unknown blob. The response format is as follows: { "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": } }, ... ] } ### Listing Repositories Images are stored in collections, known as a _repository_, which is keyed by a `name`, as seen throughout the API specification. A registry instance may contain several repositories. The list of available repositories is made available through the _catalog_. The catalog for a given registry can be retrieved with the following request: ``` GET /v2/_catalog ``` The response will be in the following format: ``` 200 OK Content-Type: application/json { "repositories": [ , ... ] } ``` Note that the contents of the response are specific to the registry implementation. Some registries may opt to provide a full catalog output, limit it based on the user's access level or omit upstream results, if providing mirroring functionality. Subsequently, the presence of a repository in the catalog listing only means that the registry *may* provide access to the repository at the time of the request. Conversely, a missing entry does *not* mean that the registry does not have the repository. More succinctly, the presence of a repository only guarantees that it is there but not that it is _not_ there. For registries with a large number of repositories, this response may be quite large. If such a response is expected, one should use pagination. A registry may also limit the amount of responses returned even if pagination was not explicitly requested. In this case the `Link` header will be returned along with the results, and subsequent results can be obtained by following the link as if pagination had been initially requested. For details of the `Link` header, please see the [_Pagination_](#pagination) section. #### Pagination Paginated catalog results can be retrieved by adding an `n` parameter to the request URL, declaring that the response should be limited to `n` results. Starting a paginated flow begins as follows: ``` GET /v2/_catalog?n= ``` The above specifies that a catalog response should be returned, from the start of the result set, ordered lexically, limiting the number of results to `n`. The response to such a request would look as follows: ``` 200 OK Content-Type: application/json Link: <?n=&last=>; rel="next" { "repositories": [ , ... ] } ``` The above includes the _first_ `n` entries from the result set. To get the _next_ `n` entries, one can create a URL where the argument `last` has the value from `repositories[len(repositories)-1]`. If there are indeed more results, the URL for the next block is encoded in an [RFC5988](https://tools.ietf.org/html/rfc5988) `Link` header, as a "next" relation. The presence of the `Link` header communicates to the client that the entire result set has not been returned and another request must be issued. If the header is not present, the client can assume that all results have been received. > __NOTE:__ In the request template above, note that the brackets > are required. For example, if the url is > `http://example.com/v2/_catalog?n=20&last=b`, the value of the header would > be `; rel="next"`. Please see > [RFC5988](https://tools.ietf.org/html/rfc5988) for details. Compliant client implementations should always use the `Link` header value when proceeding through results linearly. The client may construct URLs to skip forward in the catalog. To get the next result set, a client would issue the request as follows, using the URL encoded in the described `Link` header: ``` GET /v2/_catalog?n=&last= ``` The above process should then be repeated until the `Link` header is no longer set. The catalog result set is represented abstractly as a lexically sorted list, where the position in that list can be specified by the query term `last`. The entries in the response start _after_ the term specified by `last`, up to `n` entries. The behavior of `last` is quite simple when demonstrated with an example. Let us say the registry has the following repositories: ``` a b c d ``` If the value of `n` is 2, _a_ and _b_ will be returned on the first response. The `Link` header returned on the response will have `n` set to 2 and last set to _b_: ``` Link: <?n=2&last=b>; rel="next" ``` The client can then issue the request with the above value from the `Link` header, receiving the values _c_ and _d_. Note that `n` may change on the second to last response or be fully omitted, depending on the server implementation. ### Listing Image Tags It may be necessary to list all of the tags under a given repository. The tags for an image repository can be retrieved with the following request: GET /v2//tags/list The response will be in the following format: 200 OK Content-Type: application/json { "name": , "tags": [ , ... ] } For repositories with a large number of tags, this response may be quite large. If such a response is expected, one should use the pagination. #### Pagination Paginated tag results can be retrieved by adding the appropriate parameters to the request URL described above. The behavior of tag pagination is identical to that specified for catalog pagination. We cover a simple flow to highlight any differences. Starting a paginated flow may begin as follows: ``` GET /v2//tags/list?n= ``` The above specifies that a tags response should be returned, from the start of the result set, ordered lexically, limiting the number of results to `n`. The response to such a request would look as follows: ``` 200 OK Content-Type: application/json Link: <?n=&last=>; rel="next" { "name": , "tags": [ , ... ] } ``` To get the next result set, a client would issue the request as follows, using the value encoded in the [RFC5988](https://tools.ietf.org/html/rfc5988) `Link` header: ``` GET /v2//tags/list?n=&last= ``` The above process should then be repeated until the `Link` header is no longer set in the response. The behavior of the `last` parameter, the provided response result, lexical ordering and encoding of the `Link` header are identical to that of catalog pagination. ### Deleting an Image An image may be deleted from the registry via its `name` and `reference`. A delete may be issued with the following request format: DELETE /v2//manifests/ For deletes, `reference` *must* be a digest or the delete will fail. If the image exists and has been successfully deleted, the following response will be issued: 202 Accepted Content-Length: None If the image had already been deleted or did not exist, a `404 Not Found` response will be issued instead. > **Note** When deleting a manifest from a registry version 2.3 or later, the > following header must be used when `HEAD` or `GET`-ing the manifest to obtain > the correct digest to delete: Accept: application/vnd.docker.distribution.manifest.v2+json > for more details, see: [compatibility.md](../compatibility.md#content-addressable-storage-cas) ## Detail > **Note**: This section is still under construction. For the purposes of > implementation, if any details below differ from the described request flows > above, the section below should be corrected. When they match, this note > should be removed. The behavior of the endpoints are covered in detail in this section, organized by route and entity. All aspects of the request and responses are covered, including headers, parameters and body formats. Examples of requests and their corresponding responses, with success and failure, are enumerated. > **Note**: The sections on endpoint detail are arranged with an example > request, a description of the request, followed by information about that > request. A list of methods and URIs are covered in the table below: |Method|Path|Entity|Description| |------|----|------|-----------| {{range $route := .RouteDescriptors}}{{range $method := .Methods}}| {{$method.Method}} | `{{$route.Path|prettygorilla}}` | {{$route.Entity}} | {{$method.Description}} | {{end}}{{end}} The detail for each endpoint is covered in the following sections. ### Errors The error codes encountered via the API are enumerated in the following table: |Code|Message|Description| |----|-------|-----------| {{range $err := .ErrorDescriptors}} `{{$err.Value}}` | {{$err.Message}} | {{$err.Description|removenewlines}} {{end}} {{range $route := .RouteDescriptors}} ### {{.Entity}} {{.Description}} {{range $method := $route.Methods}} #### {{.Method}} {{$route.Entity}} {{.Description}} {{if .Requests}}{{range .Requests}}{{if .Name}} ##### {{.Name}}{{end}} ``` {{$method.Method}} {{$route.Path|prettygorilla}}{{range $i, $param := .QueryParameters}}{{if eq $i 0}}?{{else}}&{{end}}{{$param.Name}}={{$param.Format}}{{end}}{{range .Headers}} {{.Name}}: {{.Format}}{{end}}{{if .Body.ContentType}} Content-Type: {{.Body.ContentType}}{{end}}{{if .Body.Format}} {{.Body.Format}}{{end}} ``` {{.Description}} {{if or .Headers .PathParameters .QueryParameters}} The following parameters should be specified on the request: |Name|Kind|Description| |----|----|-----------| {{range .Headers}}|`{{.Name}}`|header|{{.Description}}| {{end}}{{range .PathParameters}}|`{{.Name}}`|path|{{.Description}}| {{end}}{{range .QueryParameters}}|`{{.Name}}`|query|{{.Description}}| {{end}}{{end}} {{if .Successes}} {{range .Successes}} ###### On Success: {{if .Name}}{{.Name}}{{else}}{{.StatusCode | statustext}}{{end}} ``` {{.StatusCode}} {{.StatusCode | statustext}}{{range .Headers}} {{.Name}}: {{.Format}}{{end}}{{if .Body.ContentType}} Content-Type: {{.Body.ContentType}}{{end}}{{if .Body.Format}} {{.Body.Format}}{{end}} ``` {{.Description}} {{if .Fields}}The following fields may be returned in the response body: |Name|Description| |----|-----------| {{range .Fields}}|`{{.Name}}`|{{.Description}}| {{end}}{{end}}{{if .Headers}} The following headers will be returned with the response: |Name|Description| |----|-----------| {{range .Headers}}|`{{.Name}}`|{{.Description}}| {{end}}{{end}}{{end}}{{end}} {{if .Failures}} {{range .Failures}} ###### On Failure: {{if .Name}}{{.Name}}{{else}}{{.StatusCode | statustext}}{{end}} ``` {{.StatusCode}} {{.StatusCode | statustext}}{{range .Headers}} {{.Name}}: {{.Format}}{{end}}{{if .Body.ContentType}} Content-Type: {{.Body.ContentType}}{{end}}{{if .Body.Format}} {{.Body.Format}}{{end}} ``` {{.Description}} {{if .Headers}} The following headers will be returned on the response: |Name|Description| |----|-----------| {{range .Headers}}|`{{.Name}}`|{{.Description}}| {{end}}{{end}} {{if .ErrorCodes}} The error codes that may be included in the response body are enumerated below: |Code|Message|Description| |----|-------|-----------| {{range $err := .ErrorCodes}}| `{{$err.Descriptor.Value}}` | {{$err.Descriptor.Message}} | {{$err.Descriptor.Description|removenewlines}} | {{end}} {{end}}{{end}}{{end}}{{end}}{{end}}{{end}} {{end}} docker-registry-2.6.2~ds1/docs/spec/auth/000077500000000000000000000000001313450123100202605ustar00rootroot00000000000000docker-registry-2.6.2~ds1/docs/spec/auth/index.md000066400000000000000000000007021313450123100217100ustar00rootroot00000000000000--- title: "Docker Registry Token Authentication" description: "Docker Registry v2 authentication schema" keywords: ["registry, on-prem, images, tags, repository, distribution, authentication, advanced"] --- # Docker Registry v2 authentication See the [Token Authentication Specification](token.md), [Token Authentication Implementation](jwt.md), [Token Scope Documentation](scope.md), [OAuth2 Token Authentication](oauth.md) for more information. docker-registry-2.6.2~ds1/docs/spec/auth/jwt.md000066400000000000000000000335151313450123100214150ustar00rootroot00000000000000--- title: "Token Authentication Implementation" description: "Describe the reference implementation of the Docker Registry v2 authentication schema" keywords: ["registry, on-prem, images, tags, repository, distribution, JWT authentication, advanced"] --- # Docker Registry v2 Bearer token specification This specification covers the `docker/distribution` implementation of the v2 Registry's authentication schema. Specifically, it describes the JSON Web Token schema that `docker/distribution` has adopted to implement the client-opaque Bearer token issued by an authentication service and understood by the registry. This document borrows heavily from the [JSON Web Token Draft Spec](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32) ## Getting a Bearer Token For this example, the client makes an HTTP GET request to the following URL: ``` https://auth.docker.io/token?service=registry.docker.io&scope=repository:samalba/my-app:pull,push ``` The token server should first attempt to authenticate the client using any authentication credentials provided with the request. As of Docker 1.8, the registry client in the Docker Engine only supports Basic Authentication to these token servers. If an attempt to authenticate to the token server fails, the token server should return a `401 Unauthorized` response indicating that the provided credentials are invalid. Whether the token server requires authentication is up to the policy of that access control provider. Some requests may require authentication to determine access (such as pushing or pulling a private repository) while others may not (such as pulling from a public repository). After authenticating the client (which may simply be an anonymous client if no attempt was made to authenticate), the token server must next query its access control list to determine whether the client has the requested scope. In this example request, if I have authenticated as user `jlhawn`, the token server will determine what access I have to the repository `samalba/my-app` hosted by the entity `registry.docker.io`. Once the token server has determined what access the client has to the resources requested in the `scope` parameter, it will take the intersection of the set of requested actions on each resource and the set of actions that the client has in fact been granted. If the client only has a subset of the requested access **it must not be considered an error** as it is not the responsibility of the token server to indicate authorization errors as part of this workflow. Continuing with the example request, the token server will find that the client's set of granted access to the repository is `[pull, push]` which when intersected with the requested access `[pull, push]` yields an equal set. If the granted access set was found only to be `[pull]` then the intersected set would only be `[pull]`. If the client has no access to the repository then the intersected set would be empty, `[]`. It is this intersected set of access which is placed in the returned token. The server will now construct a JSON Web Token to sign and return. A JSON Web Token has 3 main parts: 1. Headers The header of a JSON Web Token is a standard JOSE header. The "typ" field will be "JWT" and it will also contain the "alg" which identifies the signing algorithm used to produce the signature. It also must have a "kid" field, representing the ID of the key which was used to sign the token. The "kid" field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps: 1. Take the DER encoded public key which the JWT token was signed against. 2. Create a SHA256 hash out of it and truncate to 240bits. 3. Split the result into 12 base32 encoded groups with `:` as delimiter. Here is an example JOSE Header for a JSON Web Token (formatted with whitespace for readability): ``` { "typ": "JWT", "alg": "ES256", "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6" } ``` It specifies that this object is going to be a JSON Web token signed using the key with the given ID using the Elliptic Curve signature algorithm using a SHA256 hash. 2. Claim Set The Claim Set is a JSON struct containing these standard registered claim name fields:
iss (Issuer)
The issuer of the token, typically the fqdn of the authorization server.
sub (Subject)
The subject of the token; the name or id of the client which requested it. This should be empty (`""`) if the client did not authenticate.
aud (Audience)
The intended audience of the token; the name or id of the service which will verify the token to authorize the client/subject.
exp (Expiration)
The token should only be considered valid up to this specified date and time.
nbf (Not Before)
The token should not be considered valid before this specified date and time.
iat (Issued At)
Specifies the date and time which the Authorization server generated this token.
jti (JWT ID)
A unique identifier for this token. Can be used by the intended audience to prevent replays of the token.
The Claim Set will also contain a private claim name unique to this authorization server specification:
access
An array of access entry objects with the following fields:
type
The type of resource hosted by the service.
name
The name of the resource of the given type hosted by the service.
actions
An array of strings which give the actions authorized on this resource.
Here is an example of such a JWT Claim Set (formatted with whitespace for readability): ``` { "iss": "auth.docker.com", "sub": "jlhawn", "aud": "registry.docker.com", "exp": 1415387315, "nbf": 1415387015, "iat": 1415387015, "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws", "access": [ { "type": "repository", "name": "samalba/my-app", "actions": [ "pull", "push" ] } ] } ``` 3. Signature The authorization server will produce a JOSE header and Claim Set with no extraneous whitespace, i.e., the JOSE Header from above would be ``` {"typ":"JWT","alg":"ES256","kid":"PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"} ``` and the Claim Set from above would be ``` {"iss":"auth.docker.com","sub":"jlhawn","aud":"registry.docker.com","exp":1415387315,"nbf":1415387015,"iat":1415387015,"jti":"tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws","access":[{"type":"repository","name":"samalba/my-app","actions":["push","pull"]}]} ``` The utf-8 representation of this JOSE header and Claim Set are then url-safe base64 encoded (sans trailing '=' buffer), producing: ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0 ``` for the JOSE Header and ``` eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0 ``` for the Claim Set. These two are concatenated using a '.' character, yielding the string: ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0 ``` This is then used as the payload to a the `ES256` signature algorithm specified in the JOSE header and specified fully in [Section 3.4 of the JSON Web Algorithms (JWA) draft specification](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.4) This example signature will use the following ECDSA key for the server: ``` { "kty": "EC", "crv": "P-256", "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6", "d": "R7OnbfMaD5J2jl7GeE8ESo7CnHSBm_1N2k9IXYFrKJA", "x": "m7zUpx3b-zmVE5cymSs64POG9QcyEpJaYCD82-549_Q", "y": "dU3biz8sZ_8GPB-odm8Wxz3lNDr1xcAQQPQaOcr1fmc" } ``` A resulting signature of the above payload using this key is: ``` QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w ``` Concatenating all of these together with a `.` character gives the resulting JWT: ``` eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w ``` This can now be placed in an HTTP response and returned to the client to use to authenticate to the audience service: ``` HTTP/1.1 200 OK Content-Type: application/json {"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w"} ``` ## Using the signed token Once the client has a token, it will try the registry request again with the token placed in the HTTP `Authorization` header like so: ``` Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWjpVQjVaOktJQVA6SU5QTDo1RU42Ok40SjQ6Nk1XTzpEUktFOkJWUUs6M0ZKTDpQT1RMIn0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJCQ0NZOk9VNlo6UUVKNTpXTjJDOjJBVkM6WTdZRDpBM0xZOjQ1VVc6NE9HRDpLQUxMOkNOSjU6NUlVTCIsImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5jb20iLCJleHAiOjE0MTUzODczMTUsIm5iZiI6MTQxNTM4NzAxNSwiaWF0IjoxNDE1Mzg3MDE1LCJqdGkiOiJ0WUpDTzFjNmNueXk3a0FuMGM3cktQZ2JWMUgxYkZ3cyIsInNjb3BlIjoiamxoYXduOnJlcG9zaXRvcnk6c2FtYWxiYS9teS1hcHA6cHVzaCxwdWxsIGpsaGF3bjpuYW1lc3BhY2U6c2FtYWxiYTpwdWxsIn0.Y3zZSwaZPqy4y9oRBVRImZyv3m_S9XDHF1tWwN7mL52C_IiA73SJkWVNsvNqpJIn5h7A2F8biv_S2ppQ1lgkbw ``` This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1) ## Verifying the token The registry must now verify the token presented by the user by inspecting the claim set within. The registry will: - Ensure that the issuer (`iss` claim) is an authority it trusts. - Ensure that the registry identifies as the audience (`aud` claim). - Check that the current time is between the `nbf` and `exp` claim times. - If enforcing single-use tokens, check that the JWT ID (`jti` claim) value has not been seen before. - To enforce this, the registry may keep a record of `jti`s it has seen for up to the `exp` time of the token to prevent token replays. - Check the `access` claim value and use the identified resources and the list of actions authorized to determine whether the token grants the required level of access for the operation the client is attempting to perform. - Verify that the signature of the token is valid. If any of these requirements are not met, the registry will return a `403 Forbidden` response to indicate that the token is invalid. **Note**: it is only at this point in the workflow that an authorization error may occur. The token server should *not* return errors when the user does not have the requested authorization. Instead, the returned token should indicate whatever of the requested scope the client does have (the intersection of requested and granted access). If the token does not supply proper authorization then the registry will return the appropriate error. At no point in this process should the registry need to call back to the authorization server. The registry only needs to be supplied with the trusted public keys to verify the token signatures. docker-registry-2.6.2~ds1/docs/spec/auth/oauth.md000066400000000000000000000155761313450123100217400ustar00rootroot00000000000000--- title: "Oauth2 Token Authentication" description: "Specifies the Docker Registry v2 authentication" keywords: ["registry, on-prem, images, tags, repository, distribution, oauth2, advanced"] --- # Docker Registry v2 authentication using OAuth2 This document describes support for the OAuth2 protocol within the authorization server. [RFC6749](https://tools.ietf.org/html/rfc6749) should be used as a reference for the protocol and HTTP endpoints described here. **Note**: Not all token servers implement oauth2. If the request to the endpoint returns `404` using the HTTP `POST` method, refer to [Token Documentation](token.md) for using the HTTP `GET` method supported by all token servers. ## Refresh token format The format of the refresh token is completely opaque to the client and should be determined by the authorization server. The authorization should ensure the token is sufficiently long and is responsible for storing any information about long-lived tokens which may be needed for revoking. Any information stored inside the token will not be extracted and presented by clients. ## Getting a token POST /token #### Headers Content-Type: application/x-www-form-urlencoded #### Post parameters
grant_type
(REQUIRED) Type of grant used to get token. When getting a refresh token using credentials this type should be set to "password" and have the accompanying username and password paramters. Type "authorization_code" is reserved for future use for authenticating to an authorization server without having to send credentials directly from the client. When requesting an access token with a refresh token this should be set to "refresh_token".
service
(REQUIRED) The name of the service which hosts the resource to get access for. Refresh tokens will only be good for getting tokens for this service.
client_id
(REQUIRED) String identifying the client. This client_id does not need to be registered with the authorization server but should be set to a meaningful value in order to allow auditing keys created by unregistered clients. Accepted syntax is defined in [RFC6749 Appendix A.1](https://tools.ietf.org/html/rfc6749#appendix-A.1)
access_type
(OPTIONAL) Access which is being requested. If "offline" is provided then a refresh token will be returned. The default is "online" only returning short lived access token. If the grant type is "refresh_token" this will only return the same refresh token and not a new one.
scope
(OPTIONAL) The resource in question, formatted as one of the space-delimited entries from the scope parameters from the WWW-Authenticate header shown above. This query parameter should only be specified once but may contain multiple scopes using the scope list format defined in the scope grammar. If multiple scope is provided from WWW-Authenticate header the scopes should first be converted to a scope list before requesting the token. The above example would be specified as: scope=repository:samalba/my-app:push. When requesting a refresh token the scopes may be empty since the refresh token will not be limited by this scope, only the provided short lived access token will have the scope limitation.
refresh_token
(OPTIONAL) The refresh token to use for authentication when grant type "refresh_token" is used.
username
(OPTIONAL) The username to use for authentication when grant type "password" is used.
password
(OPTIONAL) The password to use for authentication when grant type "password" is used.
#### Response fields
access_token
(REQUIRED) An opaque Bearer token that clients should supply to subsequent requests in the Authorization header. This token should not be attempted to be parsed or understood by the client but treated as opaque string.
scope
(REQUIRED) The scope granted inside the access token. This may be the same scope as requested or a subset. This requirement is stronger than specified in [RFC6749 Section 4.2.2](https://tools.ietf.org/html/rfc6749#section-4.2.2) by strictly requiring the scope in the return value.
expires_in
(REQUIRED) The duration in seconds since the token was issued that it will remain valid. When omitted, this defaults to 60 seconds. For compatibility with older clients, a token should never be returned with less than 60 seconds to live.
issued_at
(Optional) The RFC3339-serialized UTC standard time at which a given token was issued. If issued_at is omitted, the expiration is from when the token exchange completed.
refresh_token
(Optional) Token which can be used to get additional access tokens for the same subject with different scopes. This token should be kept secure by the client and only sent to the authorization server which issues bearer tokens. This field will only be set when `access_type=offline` is provided in the request.
#### Example getting refresh token ``` POST /token HTTP/1.1 Host: auth.docker.io Content-Type: application/x-www-form-urlencoded grant_type=password&username=johndoe&password=A3ddj3w&service=hub.docker.io&client_id=dockerengine&access_type=offline HTTP/1.1 200 OK Content-Type: application/json {"refresh_token":"kas9Da81Dfa8","access_token":"eyJhbGciOiJFUzI1NiIsInR5","expires_in":900,"scope":""} ``` #### Example refreshing an Access Token ``` POST /token HTTP/1.1 Host: auth.docker.io Content-Type: application/x-www-form-urlencoded grant_type=refresh_token&refresh_token=kas9Da81Dfa8&service=registry-1.docker.io&client_id=dockerengine&scope=repository:samalba/my-app:pull,push HTTP/1.1 200 OK Content-Type: application/json {"refresh_token":"kas9Da81Dfa8","access_token":"eyJhbGciOiJFUzI1NiIsInR5":"expires_in":900,"scope":"repository:samalba/my-app:pull,repository:samalba/my-app:push"} ``` docker-registry-2.6.2~ds1/docs/spec/auth/scope.md000066400000000000000000000156731313450123100217270ustar00rootroot00000000000000--- title: "Token Scope Documentation" description: "Describes the scope and access fields used for registry authorization tokens" keywords: ["registry, on-prem, images, tags, repository, distribution, advanced, access, scope"] --- # Docker Registry Token Scope and Access Tokens used by the registry are always restricted what resources they may be used to access, where those resources may be accessed, and what actions may be done on those resources. Tokens always have the context of a user which the token was originally created for. This document describes how these restrictions are represented and enforced by the authorization server and resource providers. ## Scope Components ### Subject (Authenticated User) The subject represents the user for which a token is valid. Any actions performed using an access token should be considered on behalf of the subject. This is included in the `sub` field of access token JWT. A refresh token should be limited to a single subject and only be able to give out access tokens for that subject. ### Audience (Resource Provider) The audience represents a resource provider which is intended to be able to perform the actions specified in the access token. Any resource provider which does not match the audience should not use that access token. The audience is included in the `aud` field of the access token JWT. A refresh token should be limited to a single audience and only be able to give out access tokens for that audience. ### Resource Type The resource type represents the type of resource which the resource name is intended to represent. This type may be specific to a resource provider but must be understood by the authorization server in order to validate the subject is authorized for a specific resource. #### Resource Class The resource type might have a resource class which further classifies the the resource name within the resource type. A class is not required and is specific to the resource type. #### Example Resource Types - `repository` - represents a single repository within a registry. A repository may represent many manifest or content blobs, but the resource type is considered the collections of those items. Actions which may be performed on a `repository` are `pull` for accessing the collection and `push` for adding to it. By default the `repository` type has the class of `image`. - `repository(plugin)` - represents a single repository of plugins within a registry. A plugin repository has the same content and actions as a repository. - `registry` - represents the entire registry. Used for administrative actions or lookup operations that span an entire registry. ### Resource Name The resource name represent the name which identifies a resource for a resource provider. A resource is identified by this name and the provided resource type. An example of a resource name would be the name component of an image tag, such as "samalba/myapp" or "hostname/samalba/myapp". ### Resource Actions The resource actions define the actions which the access token allows to be performed on the identified resource. These actions are type specific but will normally have actions identifying read and write access on the resource. Example for the `repository` type are `pull` for read access and `push` for write access. ## Authorization Server Use Each access token request may include a scope and an audience. The subject is always derived from the passed in credentials or refresh token. When using a refresh token the passed in audience must match the audience defined for the refresh token. The audience (resource provider) is provided using the `service` field. Multiple resource scopes may be provided using multiple `scope` fields on the `GET` request. The `POST` request only takes in a single `scope` field but may use a space to separate a list of multiple resource scopes. ### Resource Scope Grammar ``` scope := resourcescope [ ' ' resourcescope ]* resourcescope := resourcetype ":" resourcename ":" action [ ',' action ]* resourcetype := resourcetypevalue [ '(' resourcetypevalue ')' ] resourcetypevalue := /[a-z0-9]+/ resourcename := [ hostname '/' ] component [ '/' component ]* hostname := hostcomponent ['.' hostcomponent]* [':' port-number] hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ port-number := /[0-9]+/ action := /[a-z]*/ component := alpha-numeric [ separator alpha-numeric ]* alpha-numeric := /[a-z0-9]+/ separator := /[_.]|__|[-]*/ ``` Full reference grammar is defined [here](https://godoc.org/github.com/docker/distribution/reference). Currently the scope name grammar is a subset of the reference grammar. > **NOTE:** that the `resourcename` may contain one `:` due to a possible port > number in the hostname component of the `resourcename`, so a naive > implementation that interprets the first three `:`-delimited tokens of a > `scope` to be the `resourcetype`, `resourcename`, and a list of `action` > would be insufficient. ## Resource Provider Use Once a resource provider has verified the authenticity of the scope through JWT access token verification, the resource provider must ensure that scope satisfies the request. The resource provider should match the given audience according to name or URI the resource provider uses to identify itself. Any denial based on subject is not defined here and is up to resource provider, the subject is mainly provided for audit logs and any other user-specific rules which may need to be provided but are not defined by the authorization server. The resource provider must ensure that ANY resource being accessed as the result of a request has the appropriate access scope. Both the resource type and resource name must match the accessed resource and an appropriate action scope must be included. When appropriate authorization is not provided either due to lack of scope or missing token, the resource provider to return a `WWW-AUTHENTICATE` HTTP header with the `realm` as the authorization server, the `service` as the expected audience identifying string, and a `scope` field for each required resource scope to complete the request. ## JWT Access Tokens Each JWT access token may only have a single subject and audience but multiple resource scopes. The subject and audience are put into standard JWT fields `sub` and `aud`. The resource scope is put into the `access` field. The structure of the access field can be seen in the [jwt documentation](jwt.md). ## Refresh Tokens A refresh token must be defined for a single subject and audience. Further restricting scope to specific type, name, and actions combinations should be done by fetching an access token using the refresh token. Since the refresh token is not scoped to specific resources for an audience, extra care should be taken to only use the refresh token to negotiate new access tokens directly with the authorization server, and never with a resource provider. docker-registry-2.6.2~ds1/docs/spec/auth/token.md000066400000000000000000000270621313450123100217310ustar00rootroot00000000000000--- title: "Token Authentication Specification" description: "Specifies the Docker Registry v2 authentication" keywords: ["registry, on-prem, images, tags, repository, distribution, Bearer authentication, advanced"] --- # Docker Registry v2 authentication via central service This document outlines the v2 Docker registry authentication scheme: ![v2 registry auth](../../images/v2-registry-auth.png) 1. Attempt to begin a push/pull operation with the registry. 2. If the registry requires authorization it will return a `401 Unauthorized` HTTP response with information on how to authenticate. 3. The registry client makes a request to the authorization service for a Bearer token. 4. The authorization service returns an opaque Bearer token representing the client's authorized access. 5. The client retries the original request with the Bearer token embedded in the request's Authorization header. 6. The Registry authorizes the client by validating the Bearer token and the claim set embedded within it and begins the push/pull session as usual. ## Requirements - Registry clients which can understand and respond to token auth challenges returned by the resource server. - An authorization server capable of managing access controls to their resources hosted by any given service (such as repositories in a Docker Registry). - A Docker Registry capable of trusting the authorization server to sign tokens which clients can use for authorization and the ability to verify these tokens for single use or for use during a sufficiently short period of time. ## Authorization Server Endpoint Descriptions The described server is meant to serve as a standalone access control manager for resources hosted by other services which wish to authenticate and manage authorizations using a separate access control manager. A service like this is used by the official Docker Registry to authenticate clients and verify their authorization to Docker image repositories. As of Docker 1.6, the registry client within the Docker Engine has been updated to handle such an authorization workflow. ## How to authenticate Registry V1 clients first contact the index to initiate a push or pull. Under the Registry V2 workflow, clients should contact the registry first. If the registry server requires authentication it will return a `401 Unauthorized` response with a `WWW-Authenticate` header detailing how to authenticate to this registry. For example, say I (username `jlhawn`) am attempting to push an image to the repository `samalba/my-app`. For the registry to authorize this, I will need `push` access to the `samalba/my-app` repository. The registry will first return this response: ``` HTTP/1.1 401 Unauthorized Content-Type: application/json; charset=utf-8 Docker-Distribution-Api-Version: registry/2.0 Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull,push" Date: Thu, 10 Sep 2015 19:32:31 GMT Content-Length: 235 Strict-Transport-Security: max-age=31536000 {"errors":[{"code":"UNAUTHORIZED","message":"access to the requested resource is not authorized","detail":[{"Type":"repository","Name":"samalba/my-app","Action":"pull"},{"Type":"repository","Name":"samalba/my-app","Action":"push"}]}]} ``` Note the HTTP Response Header indicating the auth challenge: ``` Www-Authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io",scope="repository:samalba/my-app:pull,push" ``` This format is documented in [Section 3 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-3) This challenge indicates that the registry requires a token issued by the specified token server and that the request the client is attempting will need to include sufficient access entries in its claim set. To respond to this challenge, the client will need to make a `GET` request to the URL `https://auth.docker.io/token` using the `service` and `scope` values from the `WWW-Authenticate` header. ## Requesting a Token Defines getting a bearer and refresh token using the token endpoint. #### Query Parameters
service
The name of the service which hosts the resource.
offline_token
Whether to return a refresh token along with the bearer token. A refresh token is capable of getting additional bearer tokens for the same subject with different scopes. The refresh token does not have an expiration and should be considered completely opaque to the client.
client_id
String identifying the client. This client_id does not need to be registered with the authorization server but should be set to a meaningful value in order to allow auditing keys created by unregistered clients. Accepted syntax is defined in [RFC6749 Appendix A.1](https://tools.ietf.org/html/rfc6749#appendix-A.1).
scope
The resource in question, formatted as one of the space-delimited entries from the scope parameters from the WWW-Authenticate header shown above. This query parameter should be specified multiple times if there is more than one scope entry from the WWW-Authenticate header. The above example would be specified as: scope=repository:samalba/my-app:push. The scope field may be empty to request a refresh token without providing any resource permissions to the returned bearer token.
#### Token Response Fields
token
An opaque Bearer token that clients should supply to subsequent requests in the Authorization header.
access_token
For compatibility with OAuth 2.0, we will also accept token under the name access_token. At least one of these fields must be specified, but both may also appear (for compatibility with older clients). When both are specified, they should be equivalent; if they differ the client's choice is undefined.
expires_in
(Optional) The duration in seconds since the token was issued that it will remain valid. When omitted, this defaults to 60 seconds. For compatibility with older clients, a token should never be returned with less than 60 seconds to live.
issued_at
(Optional) The RFC3339-serialized UTC standard time at which a given token was issued. If issued_at is omitted, the expiration is from when the token exchange completed.
refresh_token
(Optional) Token which can be used to get additional access tokens for the same subject with different scopes. This token should be kept secure by the client and only sent to the authorization server which issues bearer tokens. This field will only be set when `offline_token=true` is provided in the request.
#### Example For this example, the client makes an HTTP GET request to the following URL: ``` https://auth.docker.io/token?service=registry.docker.io&scope=repository:samalba/my-app:pull,push ``` The token server should first attempt to authenticate the client using any authentication credentials provided with the request. From Docker 1.11 the Docker engine supports both Basic Authentication and [OAuth2](oauth.md) for getting tokens. Docker 1.10 and before, the registry client in the Docker Engine only supports Basic Authentication. If an attempt to authenticate to the token server fails, the token server should return a `401 Unauthorized` response indicating that the provided credentials are invalid. Whether the token server requires authentication is up to the policy of that access control provider. Some requests may require authentication to determine access (such as pushing or pulling a private repository) while others may not (such as pulling from a public repository). After authenticating the client (which may simply be an anonymous client if no attempt was made to authenticate), the token server must next query its access control list to determine whether the client has the requested scope. In this example request, if I have authenticated as user `jlhawn`, the token server will determine what access I have to the repository `samalba/my-app` hosted by the entity `registry.docker.io`. Once the token server has determined what access the client has to the resources requested in the `scope` parameter, it will take the intersection of the set of requested actions on each resource and the set of actions that the client has in fact been granted. If the client only has a subset of the requested access **it must not be considered an error** as it is not the responsibility of the token server to indicate authorization errors as part of this workflow. Continuing with the example request, the token server will find that the client's set of granted access to the repository is `[pull, push]` which when intersected with the requested access `[pull, push]` yields an equal set. If the granted access set was found only to be `[pull]` then the intersected set would only be `[pull]`. If the client has no access to the repository then the intersected set would be empty, `[]`. It is this intersected set of access which is placed in the returned token. The server then constructs an implementation-specific token with this intersected set of access, and returns it to the Docker client to use to authenticate to the audience service (within the indicated window of time): ``` HTTP/1.1 200 OK Content-Type: application/json {"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IlBZWU86VEVXVTpWN0pIOjI2SlY6QVFUWjpMSkMzOlNYVko6WEdIQTozNEYyOjJMQVE6WlJNSzpaN1E2In0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJqbGhhd24iLCJhdWQiOiJyZWdpc3RyeS5kb2NrZXIuY29tIiwiZXhwIjoxNDE1Mzg3MzE1LCJuYmYiOjE0MTUzODcwMTUsImlhdCI6MTQxNTM4NzAxNSwianRpIjoidFlKQ08xYzZjbnl5N2tBbjBjN3JLUGdiVjFIMWJGd3MiLCJhY2Nlc3MiOlt7InR5cGUiOiJyZXBvc2l0b3J5IiwibmFtZSI6InNhbWFsYmEvbXktYXBwIiwiYWN0aW9ucyI6WyJwdXNoIl19XX0.QhflHPfbd6eVF4lM9bwYpFZIV0PfikbyXuLx959ykRTBpe3CYnzs6YBK8FToVb5R47920PVLrh8zuLzdCr9t3w", "expires_in": 3600,"issued_at": "2009-11-10T23:00:00Z"} ``` ## Using the Bearer token Once the client has a token, it will try the registry request again with the token placed in the HTTP `Authorization` header like so: ``` Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6IkJWM0Q6MkFWWjpVQjVaOktJQVA6SU5QTDo1RU42Ok40SjQ6Nk1XTzpEUktFOkJWUUs6M0ZKTDpQT1RMIn0.eyJpc3MiOiJhdXRoLmRvY2tlci5jb20iLCJzdWIiOiJCQ0NZOk9VNlo6UUVKNTpXTjJDOjJBVkM6WTdZRDpBM0xZOjQ1VVc6NE9HRDpLQUxMOkNOSjU6NUlVTCIsImF1ZCI6InJlZ2lzdHJ5LmRvY2tlci5jb20iLCJleHAiOjE0MTUzODczMTUsIm5iZiI6MTQxNTM4NzAxNSwiaWF0IjoxNDE1Mzg3MDE1LCJqdGkiOiJ0WUpDTzFjNmNueXk3a0FuMGM3cktQZ2JWMUgxYkZ3cyIsInNjb3BlIjoiamxoYXduOnJlcG9zaXRvcnk6c2FtYWxiYS9teS1hcHA6cHVzaCxwdWxsIGpsaGF3bjpuYW1lc3BhY2U6c2FtYWxiYTpwdWxsIn0.Y3zZSwaZPqy4y9oRBVRImZyv3m_S9XDHF1tWwN7mL52C_IiA73SJkWVNsvNqpJIn5h7A2F8biv_S2ppQ1lgkbw ``` This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage](https://tools.ietf.org/html/rfc6750#section-2.1) docker-registry-2.6.2~ds1/docs/spec/implementations.md000066400000000000000000000012731313450123100230540ustar00rootroot00000000000000--- published: false --- # Distribution API Implementations This is a list of known implementations of the Distribution API spec. ## [Docker Distribution Registry](https://github.com/docker/distribution) Docker distribution is the reference implementation of the distribution API specification. It aims to fully implement the entire specification. ### Releases #### 2.0.1 (_in development_) Implements API 2.0.1 _Known Issues_ - No resumable push support - Content ranges ignored - Blob upload status will always return a starting range of 0 #### 2.0.0 Implements API 2.0.0 _Known Issues_ - No resumable push support - No PATCH implementation for blob upload - Content ranges ignored docker-registry-2.6.2~ds1/docs/spec/index.md000066400000000000000000000005361313450123100207540ustar00rootroot00000000000000--- title: "Reference Overview" description: "Explains registry JSON objects" keywords: ["registry, service, images, repository, json"] --- # Docker Registry Reference * [HTTP API V2](api.md) * [Storage Driver](../storage-drivers/index.md) * [Token Authentication Specification](auth/token.md) * [Token Authentication Implementation](auth/jwt.md) docker-registry-2.6.2~ds1/docs/spec/json.md000066400000000000000000000052351313450123100206170ustar00rootroot00000000000000--- published: false title: "Docker Distribution JSON Canonicalization" description: "Explains registry JSON objects" keywords: ["registry, service, images, repository, json"] --- # Docker Distribution JSON Canonicalization To provide consistent content hashing of JSON objects throughout Docker Distribution APIs, the specification defines a canonical JSON format. Adopting such a canonicalization also aids in caching JSON responses. Note that protocols should not be designed to depend on identical JSON being generated across different versions or clients. The canonicalization rules are merely useful for caching and consistency. ## Rules Compliant JSON should conform to the following rules: 1. All generated JSON should comply with [RFC 7159](http://www.ietf.org/rfc/rfc7159.txt). 2. Resulting "JSON text" shall always be encoded in UTF-8. 3. Unless a canonical key order is defined for a particular schema, object keys shall always appear in lexically sorted order. 4. All whitespace between tokens should be removed. 5. No "trailing commas" are allowed in object or array definitions. 6. The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e". Ampersand "&" is escaped to "\u0026". ## Examples The following is a simple example of a canonicalized JSON string: ```json {"asdf":1,"qwer":[],"zxcv":[{},true,1000000000,"tyui"]} ``` ## Reference ### Other Canonicalizations The OLPC project specifies [Canonical JSON](http://wiki.laptop.org/go/Canonical_JSON). While this is used in [TUF](http://theupdateframework.com/), which may be used with other distribution-related protocols, this alternative format has been proposed in case the original source changes. Specifications complying with either this specification or an alternative should explicitly call out the canonicalization format. Except for key ordering, this specification is mostly compatible. ### Go In Go, the [`encoding/json`](http://golang.org/pkg/encoding/json/) library will emit canonical JSON by default. Simply using `json.Marshal` will suffice in most cases: ```go incoming := map[string]interface{}{ "asdf": 1, "qwer": []interface{}{}, "zxcv": []interface{}{ map[string]interface{}{}, true, int(1e9), "tyui", }, } canonical, err := json.Marshal(incoming) if err != nil { // ... handle error } ``` To apply canonical JSON format spacing to an existing serialized JSON buffer, one can use [`json.Indent`](http://golang.org/src/encoding/json/indent.go?s=1918:1989#L65) with the following arguments: ```go incoming := getBytes() var canonical bytes.Buffer if err := json.Indent(&canonical, incoming, "", ""); err != nil { // ... handle error } ``` docker-registry-2.6.2~ds1/docs/spec/manifest-v2-1.md000066400000000000000000000207571313450123100221450ustar00rootroot00000000000000--- title: "Image Manifest V 2, Schema 1 " description: "image manifest for the Registry." keywords: ["registry, on-prem, images, tags, repository, distribution, api, advanced, manifest"] --- # Image Manifest Version 2, Schema 1 This document outlines the format of of the V2 image manifest. The image manifest described herein was introduced in the Docker daemon in the [v1.3.0 release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453). It is a provisional manifest to provide a compatibility with the [V1 Image format](https://github.com/docker/docker/blob/master/image/spec/v1.md), as the requirements are defined for the [V2 Schema 2 image](https://github.com/docker/distribution/pull/62). Image manifests describe the various constituents of a docker image. Image manifests can be serialized to JSON format with the following media types: Manifest Type | Media Type ------------- | ------------- manifest | "application/vnd.docker.distribution.manifest.v1+json" signed manifest | "application/vnd.docker.distribution.manifest.v1+prettyjws" *Note that "application/json" will also be accepted for schema 1.* References: - [Proposal: JSON Registry API V2.1](https://github.com/docker/docker/issues/9015) - [Proposal: Provenance step 1 - Transform images for validation and verification](https://github.com/docker/docker/issues/8093) ## *Manifest* Field Descriptions Manifest provides the base accessible fields for working with V2 image format in the registry. - **`name`** *string* name is the name of the image's repository - **`tag`** *string* tag is the tag of the image - **`architecture`** *string* architecture is the host architecture on which this image is intended to run. This is for information purposes and not currently used by the engine - **`fsLayers`** *array* fsLayers is a list of filesystem layer blob sums contained in this image. An fsLayer is a struct consisting of the following fields - **`blobSum`** *digest.Digest* blobSum is the digest of the referenced filesystem image layer. A digest must be a sha256 hash. - **`history`** *array* history is a list of unstructured historical data for v1 compatibility. It contains ID of the image layer and ID of the layer's parent layers. history is a struct consisting of the following fields - **`v1Compatibility`** string V1Compatibility is the raw V1 compatibility information. This will contain the JSON object describing the V1 of this image. - **`schemaVersion`** *int* SchemaVersion is the image manifest schema that this image follows. >**Note**:the length of `history` must be equal to the length of `fsLayers` and >entries in each are correlated by index. ## Signed Manifests Signed manifests provides an envelope for a signed image manifest. A signed manifest consists of an image manifest along with an additional field containing the signature of the manifest. The docker client can verify signed manifests and displays a message to the user. ### Signing Manifests Image manifests can be signed in two different ways: with a *libtrust* private key or an x509 certificate chain. When signing with an x509 certificate chain, the public key of the first element in the chain must be the public key corresponding with the sign key. ### Signed Manifest Field Description Signed manifests include an image manifest and a list of signatures generated by *libtrust*. A signature consists of the following fields: - **`header`** *[JOSE](http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2)* A [JSON Web Signature](http://self-issued.info/docs/draft-ietf-jose-json-web-signature.html) - **`signature`** *string* A signature for the image manifest, signed by a *libtrust* private key - **`protected`** *string* The signed protected header ## Example Manifest *Example showing the official 'hello-world' image manifest.* ``` { "name": "hello-world", "tag": "latest", "architecture": "amd64", "fsLayers": [ { "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" }, { "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" }, { "blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11" }, { "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" } ], "history": [ { "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" }, { "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n" }, ], "schemaVersion": 1, "signatures": [ { "header": { "jwk": { "crv": "P-256", "kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4", "kty": "EC", "x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A", "y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010" }, "alg": "ES256" }, "signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg", "protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ" } ] } ``` docker-registry-2.6.2~ds1/docs/spec/manifest-v2-2.md000066400000000000000000000263041313450123100221400ustar00rootroot00000000000000--- title: "Image Manifest V 2, Schema 2 " description: "image manifest for the Registry." keywords: ["registry, on-prem, images, tags, repository, distribution, api, advanced, manifest"] --- # Image Manifest Version 2, Schema 2 This document outlines the format of of the V2 image manifest, schema version 2. The original (and provisional) image manifest for V2 (schema 1), was introduced in the Docker daemon in the [v1.3.0 release](https://github.com/docker/docker/commit/9f482a66ab37ec396ac61ed0c00d59122ac07453) and is specified in the [schema 1 manifest definition](manifest-v2-1.md) This second schema version has two primary goals. The first is to allow multi-architecture images, through a "fat manifest" which references image manifests for platform-specific versions of an image. The second is to move the Docker engine towards content-addressable images, by supporting an image model where the image's configuration can be hashed to generate an ID for the image. # Media Types The following media types are used by the manifest formats described here, and the resources they reference: - `application/vnd.docker.distribution.manifest.v1+json`: schema1 (existing manifest format) - `application/vnd.docker.distribution.manifest.v2+json`: New image manifest format (schemaVersion = 2) - `application/vnd.docker.distribution.manifest.list.v2+json`: Manifest list, aka "fat manifest" - `application/vnd.docker.container.image.v1+json`: Container config JSON - `application/vnd.docker.image.rootfs.diff.tar.gzip`: "Layer", as a gzipped tar - `application/vnd.docker.image.rootfs.foreign.diff.tar.gzip`: "Layer", as a gzipped tar that should never be pushed - `application/vnd.docker.plugin.v1+json`: Plugin config JSON ## Manifest List The manifest list is the "fat manifest" which points to specific image manifests for one or more platforms. Its use is optional, and relatively few images will use one of these manifests. A client will distinguish a manifest list from an image manifest based on the Content-Type returned in the HTTP response. ## *Manifest List* Field Descriptions - **`schemaVersion`** *int* This field specifies the image manifest schema version as an integer. This schema uses the version `2`. - **`mediaType`** *string* The MIME type of the manifest list. This should be set to `application/vnd.docker.distribution.manifest.list.v2+json`. - **`manifests`** *array* The manifests field contains a list of manifests for specific platforms. Fields of an object in the manifests list are: - **`mediaType`** *string* The MIME type of the referenced object. This will generally be `application/vnd.docker.image.manifest.v2+json`, but it could also be `application/vnd.docker.image.manifest.v1+json` if the manifest list references a legacy schema-1 manifest. - **`size`** *int* The size in bytes of the object. This field exists so that a client will have an expected size for the content before validating. If the length of the retrieved content does not match the specified length, the content should not be trusted. - **`digest`** *string* The digest of the content, as defined by the [Registry V2 HTTP API Specificiation](api.md#digest-parameter). - **`platform`** *object* The platform object describes the platform which the image in the manifest runs on. A full list of valid operating system and architecture values are listed in the [Go language documentation for `$GOOS` and `$GOARCH`](https://golang.org/doc/install/source#environment) - **`architecture`** *string* The architecture field specifies the CPU architecture, for example `amd64` or `ppc64le`. - **`os`** *string* The os field specifies the operating system, for example `linux` or `windows`. - **`os.version`** *string* The optional os.version field specifies the operating system version, for example `10.0.10586`. - **`os.features`** *array* The optional os.features field specifies an array of strings, each listing a required OS feature (for example on Windows `win32k`). - **`variant`** *string* The optional variant field specifies a variant of the CPU, for example `armv6l` to specify a particular CPU variant of the ARM CPU. - **`features`** *array* The optional features field specifies an array of strings, each listing a required CPU feature (for example `sse4` or `aes`). ## Example Manifest List *Example showing a simple manifest list pointing to image manifests for two platforms:* ```json { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.image.manifest.v2+json", "size": 7143, "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f", "platform": { "architecture": "ppc64le", "os": "linux", } }, { "mediaType": "application/vnd.docker.image.manifest.v2+json", "size": 7682, "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270", "platform": { "architecture": "amd64", "os": "linux", "features": [ "sse4" ] } } ] } ``` # Image Manifest The image manifest provides a configuration and a set of layers for a container image. It's the direct replacement for the schema-1 manifest. ## *Image Manifest* Field Descriptions - **`schemaVersion`** *int* This field specifies the image manifest schema version as an integer. This schema uses version `2`. - **`mediaType`** *string* The MIME type of the manifest. This should be set to `application/vnd.docker.distribution.manifest.v2+json`. - **`config`** *object* The config field references a configuration object for a container, by digest. This configuration item is a JSON blob that the runtime uses to set up the container. This new schema uses a tweaked version of this configuration to allow image content-addressability on the daemon side. Fields of a config object are: - **`mediaType`** *string* The MIME type of the referenced object. This should generally be `application/vnd.docker.container.image.v1+json`. - **`size`** *int* The size in bytes of the object. This field exists so that a client will have an expected size for the content before validating. If the length of the retrieved content does not match the specified length, the content should not be trusted. - **`digest`** *string* The digest of the content, as defined by the [Registry V2 HTTP API Specificiation](api.md#digest-parameter). - **`layers`** *array* The layer list is ordered starting from the base image (opposite order of schema1). Fields of an item in the layers list are: - **`mediaType`** *string* The MIME type of the referenced object. This should generally be `application/vnd.docker.image.rootfs.diff.tar.gzip`. Layers of type `application/vnd.docker.image.rootfs.foreign.diff.tar.gzip` may be pulled from a remote location but they should never be pushed. - **`size`** *int* The size in bytes of the object. This field exists so that a client will have an expected size for the content before validating. If the length of the retrieved content does not match the specified length, the content should not be trusted. - **`digest`** *string* The digest of the content, as defined by the [Registry V2 HTTP API Specificiation](api.md#digest-parameter). - **`urls`** *array* Provides a list of URLs from which the content may be fetched. Content should be verified against the `digest` and `size`. This field is optional and uncommon. ## Example Image Manifest *Example showing an image manifest:* ```json { "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 7023, "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 32654, "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 16724, "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b" }, { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 73109, "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736" } ] } ``` # Backward compatibility The registry will continue to accept uploads of manifests in both the old and new formats. When pushing images, clients which support the new manifest format should first construct a manifest in the new format. If uploading this manifest fails, presumably because the registry only supports the old format, the client may fall back to uploading a manifest in the old format. When pulling images, clients indicate support for this new version of the manifest format by sending the `application/vnd.docker.distribution.manifest.v2+json` and `application/vnd.docker.distribution.manifest.list.v2+json` media types in an `Accept` header when making a request to the `manifests` endpoint. Updated clients should check the `Content-Type` header to see whether the manifest returned from the endpoint is in the old format, or is an image manifest or manifest list in the new format. If the manifest being requested uses the new format, and the appropriate media type is not present in an `Accept` header, the registry will assume that the client cannot handle the manifest as-is, and rewrite it on the fly into the old format. If the object that would otherwise be returned is a manifest list, the registry will look up the appropriate manifest for the amd64 platform and linux OS, rewrite that manifest into the old format if necessary, and return the result to the client. If no suitable manifest is found in the manifest list, the registry will return a 404 error. One of the challenges in rewriting manifests to the old format is that the old format involves an image configuration for each layer in the manifest, but the new format only provides one image configuration. To work around this, the registry will create synthetic image configurations for all layers except the top layer. These image configurations will not result in runnable images on their own, but only serve to fill in the parent chain in a compatible way. The IDs in these synthetic configurations will be derived from hashes of their respective blobs. The registry will create these configurations and their IDs using the same scheme as Docker 1.10 when it creates a legacy manifest to push to a registry which doesn't support the new format. docker-registry-2.6.2~ds1/docs/spec/menu.md000066400000000000000000000002601313450123100206030ustar00rootroot00000000000000--- title: "Reference" description: "Explains registry JSON objects" keywords: ["registry, service, images, repository, json"] type: "menu" identifier: "smn_registry_ref" --- docker-registry-2.6.2~ds1/errors.go000066400000000000000000000063361313450123100173100ustar00rootroot00000000000000package distribution import ( "errors" "fmt" "strings" "github.com/docker/distribution/digest" ) // ErrAccessDenied is returned when an access to a requested resource is // denied. var ErrAccessDenied = errors.New("access denied") // ErrManifestNotModified is returned when a conditional manifest GetByTag // returns nil due to the client indicating it has the latest version var ErrManifestNotModified = errors.New("manifest not modified") // ErrUnsupported is returned when an unimplemented or unsupported action is // performed var ErrUnsupported = errors.New("operation unsupported") // ErrTagUnknown is returned if the given tag is not known by the tag service type ErrTagUnknown struct { Tag string } func (err ErrTagUnknown) Error() string { return fmt.Sprintf("unknown tag=%s", err.Tag) } // ErrRepositoryUnknown is returned if the named repository is not known by // the registry. type ErrRepositoryUnknown struct { Name string } func (err ErrRepositoryUnknown) Error() string { return fmt.Sprintf("unknown repository name=%s", err.Name) } // ErrRepositoryNameInvalid should be used to denote an invalid repository // name. Reason may set, indicating the cause of invalidity. type ErrRepositoryNameInvalid struct { Name string Reason error } func (err ErrRepositoryNameInvalid) Error() string { return fmt.Sprintf("repository name %q invalid: %v", err.Name, err.Reason) } // ErrManifestUnknown is returned if the manifest is not known by the // registry. type ErrManifestUnknown struct { Name string Tag string } func (err ErrManifestUnknown) Error() string { return fmt.Sprintf("unknown manifest name=%s tag=%s", err.Name, err.Tag) } // ErrManifestUnknownRevision is returned when a manifest cannot be found by // revision within a repository. type ErrManifestUnknownRevision struct { Name string Revision digest.Digest } func (err ErrManifestUnknownRevision) Error() string { return fmt.Sprintf("unknown manifest name=%s revision=%s", err.Name, err.Revision) } // ErrManifestUnverified is returned when the registry is unable to verify // the manifest. type ErrManifestUnverified struct{} func (ErrManifestUnverified) Error() string { return fmt.Sprintf("unverified manifest") } // ErrManifestVerification provides a type to collect errors encountered // during manifest verification. Currently, it accepts errors of all types, // but it may be narrowed to those involving manifest verification. type ErrManifestVerification []error func (errs ErrManifestVerification) Error() string { var parts []string for _, err := range errs { parts = append(parts, err.Error()) } return fmt.Sprintf("errors verifying manifest: %v", strings.Join(parts, ",")) } // ErrManifestBlobUnknown returned when a referenced blob cannot be found. type ErrManifestBlobUnknown struct { Digest digest.Digest } func (err ErrManifestBlobUnknown) Error() string { return fmt.Sprintf("unknown blob %v on manifest", err.Digest) } // ErrManifestNameInvalid should be used to denote an invalid manifest // name. Reason may set, indicating the cause of invalidity. type ErrManifestNameInvalid struct { Name string Reason error } func (err ErrManifestNameInvalid) Error() string { return fmt.Sprintf("manifest name %q invalid: %v", err.Name, err.Reason) } docker-registry-2.6.2~ds1/health/000077500000000000000000000000001313450123100167025ustar00rootroot00000000000000docker-registry-2.6.2~ds1/health/api/000077500000000000000000000000001313450123100174535ustar00rootroot00000000000000docker-registry-2.6.2~ds1/health/api/api.go000066400000000000000000000015201313450123100205510ustar00rootroot00000000000000package api import ( "errors" "net/http" "github.com/docker/distribution/health" ) var ( updater = health.NewStatusUpdater() ) // DownHandler registers a manual_http_status that always returns an Error func DownHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { updater.Update(errors.New("Manual Check")) } else { w.WriteHeader(http.StatusNotFound) } } // UpHandler registers a manual_http_status that always returns nil func UpHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { updater.Update(nil) } else { w.WriteHeader(http.StatusNotFound) } } // init sets up the two endpoints to bring the service up and down func init() { health.Register("manual_http_status", updater) http.HandleFunc("/debug/health/down", DownHandler) http.HandleFunc("/debug/health/up", UpHandler) } docker-registry-2.6.2~ds1/health/api/api_test.go000066400000000000000000000042301313450123100216110ustar00rootroot00000000000000package api import ( "net/http" "net/http/httptest" "testing" "github.com/docker/distribution/health" ) // TestGETDownHandlerDoesNotChangeStatus ensures that calling the endpoint // /debug/health/down with METHOD GET returns a 404 func TestGETDownHandlerDoesNotChangeStatus(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "https://fakeurl.com/debug/health/down", nil) if err != nil { t.Errorf("Failed to create request.") } DownHandler(recorder, req) if recorder.Code != 404 { t.Errorf("Did not get a 404.") } } // TestGETUpHandlerDoesNotChangeStatus ensures that calling the endpoint // /debug/health/down with METHOD GET returns a 404 func TestGETUpHandlerDoesNotChangeStatus(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "https://fakeurl.com/debug/health/up", nil) if err != nil { t.Errorf("Failed to create request.") } DownHandler(recorder, req) if recorder.Code != 404 { t.Errorf("Did not get a 404.") } } // TestPOSTDownHandlerChangeStatus ensures the endpoint /debug/health/down changes // the status code of the response to 503 // This test is order dependent, and should come before TestPOSTUpHandlerChangeStatus func TestPOSTDownHandlerChangeStatus(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("POST", "https://fakeurl.com/debug/health/down", nil) if err != nil { t.Errorf("Failed to create request.") } DownHandler(recorder, req) if recorder.Code != 200 { t.Errorf("Did not get a 200.") } if len(health.CheckStatus()) != 1 { t.Errorf("DownHandler didn't add an error check.") } } // TestPOSTUpHandlerChangeStatus ensures the endpoint /debug/health/up changes // the status code of the response to 200 func TestPOSTUpHandlerChangeStatus(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("POST", "https://fakeurl.com/debug/health/up", nil) if err != nil { t.Errorf("Failed to create request.") } UpHandler(recorder, req) if recorder.Code != 200 { t.Errorf("Did not get a 200.") } if len(health.CheckStatus()) != 0 { t.Errorf("UpHandler didn't remove the error check.") } } docker-registry-2.6.2~ds1/health/checks/000077500000000000000000000000001313450123100201425ustar00rootroot00000000000000docker-registry-2.6.2~ds1/health/checks/checks.go000066400000000000000000000030451313450123100217330ustar00rootroot00000000000000package checks import ( "errors" "net" "net/http" "os" "strconv" "time" "github.com/docker/distribution/health" ) // FileChecker checks the existence of a file and returns an error // if the file exists. func FileChecker(f string) health.Checker { return health.CheckFunc(func() error { if _, err := os.Stat(f); err == nil { return errors.New("file exists") } return nil }) } // HTTPChecker does a HEAD request and verifies that the HTTP status code // returned matches statusCode. func HTTPChecker(r string, statusCode int, timeout time.Duration, headers http.Header) health.Checker { return health.CheckFunc(func() error { client := http.Client{ Timeout: timeout, } req, err := http.NewRequest("HEAD", r, nil) if err != nil { return errors.New("error creating request: " + r) } for headerName, headerValues := range headers { for _, headerValue := range headerValues { req.Header.Add(headerName, headerValue) } } response, err := client.Do(req) if err != nil { return errors.New("error while checking: " + r) } if response.StatusCode != statusCode { return errors.New("downstream service returned unexpected status: " + strconv.Itoa(response.StatusCode)) } return nil }) } // TCPChecker attempts to open a TCP connection. func TCPChecker(addr string, timeout time.Duration) health.Checker { return health.CheckFunc(func() error { conn, err := net.DialTimeout("tcp", addr, timeout) if err != nil { return errors.New("connection to " + addr + " failed") } conn.Close() return nil }) } docker-registry-2.6.2~ds1/health/checks/checks_test.go000066400000000000000000000012421313450123100227670ustar00rootroot00000000000000package checks import ( "testing" ) func TestFileChecker(t *testing.T) { if err := FileChecker("/tmp").Check(); err == nil { t.Errorf("/tmp was expected as exists") } if err := FileChecker("NoSuchFileFromMoon").Check(); err != nil { t.Errorf("NoSuchFileFromMoon was expected as not exists, error:%v", err) } } func TestHTTPChecker(t *testing.T) { if err := HTTPChecker("https://www.google.cybertron", 200, 0, nil).Check(); err == nil { t.Errorf("Google on Cybertron was expected as not exists") } if err := HTTPChecker("https://www.google.pt", 200, 0, nil).Check(); err != nil { t.Errorf("Google at Portugal was expected as exists, error:%v", err) } } docker-registry-2.6.2~ds1/health/doc.go000066400000000000000000000124451313450123100200040ustar00rootroot00000000000000// Package health provides a generic health checking framework. // The health package works expvar style. By importing the package the debug // server is getting a "/debug/health" endpoint that returns the current // status of the application. // If there are no errors, "/debug/health" will return an HTTP 200 status, // together with an empty JSON reply "{}". If there are any checks // with errors, the JSON reply will include all the failed checks, and the // response will be have an HTTP 503 status. // // A Check can either be run synchronously, or asynchronously. We recommend // that most checks are registered as an asynchronous check, so a call to the // "/debug/health" endpoint always returns immediately. This pattern is // particularly useful for checks that verify upstream connectivity or // database status, since they might take a long time to return/timeout. // // Installing // // To install health, just import it in your application: // // import "github.com/docker/distribution/health" // // You can also (optionally) import "health/api" that will add two convenience // endpoints: "/debug/health/down" and "/debug/health/up". These endpoints add // "manual" checks that allow the service to quickly be brought in/out of // rotation. // // import _ "github.com/docker/distribution/registry/health/api" // // # curl localhost:5001/debug/health // {} // # curl -X POST localhost:5001/debug/health/down // # curl localhost:5001/debug/health // {"manual_http_status":"Manual Check"} // // After importing these packages to your main application, you can start // registering checks. // // Registering Checks // // The recommended way of registering checks is using a periodic Check. // PeriodicChecks run on a certain schedule and asynchronously update the // status of the check. This allows CheckStatus to return without blocking // on an expensive check. // // A trivial example of a check that runs every 5 seconds and shuts down our // server if the current minute is even, could be added as follows: // // func currentMinuteEvenCheck() error { // m := time.Now().Minute() // if m%2 == 0 { // return errors.New("Current minute is even!") // } // return nil // } // // health.RegisterPeriodicFunc("minute_even", currentMinuteEvenCheck, time.Second*5) // // Alternatively, you can also make use of "RegisterPeriodicThresholdFunc" to // implement the exact same check, but add a threshold of failures after which // the check will be unhealthy. This is particularly useful for flaky Checks, // ensuring some stability of the service when handling them. // // health.RegisterPeriodicThresholdFunc("minute_even", currentMinuteEvenCheck, time.Second*5, 4) // // The lowest-level way to interact with the health package is calling // "Register" directly. Register allows you to pass in an arbitrary string and // something that implements "Checker" and runs your check. If your method // returns an error with nil, it is considered a healthy check, otherwise it // will make the health check endpoint "/debug/health" start returning a 503 // and list the specific check that failed. // // Assuming you wish to register a method called "currentMinuteEvenCheck() // error" you could do that by doing: // // health.Register("even_minute", health.CheckFunc(currentMinuteEvenCheck)) // // CheckFunc is a convenience type that implements Checker. // // Another way of registering a check could be by using an anonymous function // and the convenience method RegisterFunc. An example that makes the status // endpoint always return an error: // // health.RegisterFunc("my_check", func() error { // return Errors.new("This is an error!") // })) // // Examples // // You could also use the health checker mechanism to ensure your application // only comes up if certain conditions are met, or to allow the developer to // take the service out of rotation immediately. An example that checks // database connectivity and immediately takes the server out of rotation on // err: // // updater = health.NewStatusUpdater() // health.RegisterFunc("database_check", func() error { // return updater.Check() // })) // // conn, err := Connect(...) // database call here // if err != nil { // updater.Update(errors.New("Error connecting to the database: " + err.Error())) // } // // You can also use the predefined Checkers that come included with the health // package. First, import the checks: // // import "github.com/docker/distribution/health/checks // // After that you can make use of any of the provided checks. An example of // using a `FileChecker` to take the application out of rotation if a certain // file exists can be done as follows: // // health.Register("fileChecker", health.PeriodicChecker(checks.FileChecker("/tmp/disable"), time.Second*5)) // // After registering the check, it is trivial to take an application out of // rotation from the console: // // # curl localhost:5001/debug/health // {} // # touch /tmp/disable // # curl localhost:5001/debug/health // {"fileChecker":"file exists"} // // You could also test the connectivity to a downstream service by using a // "HTTPChecker", but ensure that you only mark the test unhealthy if there // are a minimum of two failures in a row: // // health.Register("httpChecker", health.PeriodicThresholdChecker(checks.HTTPChecker("https://www.google.pt"), time.Second*5, 2)) package health docker-registry-2.6.2~ds1/health/health.go000066400000000000000000000213651313450123100205050ustar00rootroot00000000000000package health import ( "encoding/json" "fmt" "net/http" "sync" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" ) // A Registry is a collection of checks. Most applications will use the global // registry defined in DefaultRegistry. However, unit tests may need to create // separate registries to isolate themselves from other tests. type Registry struct { mu sync.RWMutex registeredChecks map[string]Checker } // NewRegistry creates a new registry. This isn't necessary for normal use of // the package, but may be useful for unit tests so individual tests have their // own set of checks. func NewRegistry() *Registry { return &Registry{ registeredChecks: make(map[string]Checker), } } // DefaultRegistry is the default registry where checks are registered. It is // the registry used by the HTTP handler. var DefaultRegistry *Registry // Checker is the interface for a Health Checker type Checker interface { // Check returns nil if the service is okay. Check() error } // CheckFunc is a convenience type to create functions that implement // the Checker interface type CheckFunc func() error // Check Implements the Checker interface to allow for any func() error method // to be passed as a Checker func (cf CheckFunc) Check() error { return cf() } // Updater implements a health check that is explicitly set. type Updater interface { Checker // Update updates the current status of the health check. Update(status error) } // updater implements Checker and Updater, providing an asynchronous Update // method. // This allows us to have a Checker that returns the Check() call immediately // not blocking on a potentially expensive check. type updater struct { mu sync.Mutex status error } // Check implements the Checker interface func (u *updater) Check() error { u.mu.Lock() defer u.mu.Unlock() return u.status } // Update implements the Updater interface, allowing asynchronous access to // the status of a Checker. func (u *updater) Update(status error) { u.mu.Lock() defer u.mu.Unlock() u.status = status } // NewStatusUpdater returns a new updater func NewStatusUpdater() Updater { return &updater{} } // thresholdUpdater implements Checker and Updater, providing an asynchronous Update // method. // This allows us to have a Checker that returns the Check() call immediately // not blocking on a potentially expensive check. type thresholdUpdater struct { mu sync.Mutex status error threshold int count int } // Check implements the Checker interface func (tu *thresholdUpdater) Check() error { tu.mu.Lock() defer tu.mu.Unlock() if tu.count >= tu.threshold { return tu.status } return nil } // thresholdUpdater implements the Updater interface, allowing asynchronous // access to the status of a Checker. func (tu *thresholdUpdater) Update(status error) { tu.mu.Lock() defer tu.mu.Unlock() if status == nil { tu.count = 0 } else if tu.count < tu.threshold { tu.count++ } tu.status = status } // NewThresholdStatusUpdater returns a new thresholdUpdater func NewThresholdStatusUpdater(t int) Updater { return &thresholdUpdater{threshold: t} } // PeriodicChecker wraps an updater to provide a periodic checker func PeriodicChecker(check Checker, period time.Duration) Checker { u := NewStatusUpdater() go func() { t := time.NewTicker(period) for { <-t.C u.Update(check.Check()) } }() return u } // PeriodicThresholdChecker wraps an updater to provide a periodic checker that // uses a threshold before it changes status func PeriodicThresholdChecker(check Checker, period time.Duration, threshold int) Checker { tu := NewThresholdStatusUpdater(threshold) go func() { t := time.NewTicker(period) for { <-t.C tu.Update(check.Check()) } }() return tu } // CheckStatus returns a map with all the current health check errors func (registry *Registry) CheckStatus() map[string]string { // TODO(stevvooe) this needs a proper type registry.mu.RLock() defer registry.mu.RUnlock() statusKeys := make(map[string]string) for k, v := range registry.registeredChecks { err := v.Check() if err != nil { statusKeys[k] = err.Error() } } return statusKeys } // CheckStatus returns a map with all the current health check errors from the // default registry. func CheckStatus() map[string]string { return DefaultRegistry.CheckStatus() } // Register associates the checker with the provided name. func (registry *Registry) Register(name string, check Checker) { if registry == nil { registry = DefaultRegistry } registry.mu.Lock() defer registry.mu.Unlock() _, ok := registry.registeredChecks[name] if ok { panic("Check already exists: " + name) } registry.registeredChecks[name] = check } // Register associates the checker with the provided name in the default // registry. func Register(name string, check Checker) { DefaultRegistry.Register(name, check) } // RegisterFunc allows the convenience of registering a checker directly from // an arbitrary func() error. func (registry *Registry) RegisterFunc(name string, check func() error) { registry.Register(name, CheckFunc(check)) } // RegisterFunc allows the convenience of registering a checker in the default // registry directly from an arbitrary func() error. func RegisterFunc(name string, check func() error) { DefaultRegistry.RegisterFunc(name, check) } // RegisterPeriodicFunc allows the convenience of registering a PeriodicChecker // from an arbitrary func() error. func (registry *Registry) RegisterPeriodicFunc(name string, period time.Duration, check CheckFunc) { registry.Register(name, PeriodicChecker(CheckFunc(check), period)) } // RegisterPeriodicFunc allows the convenience of registering a PeriodicChecker // in the default registry from an arbitrary func() error. func RegisterPeriodicFunc(name string, period time.Duration, check CheckFunc) { DefaultRegistry.RegisterPeriodicFunc(name, period, check) } // RegisterPeriodicThresholdFunc allows the convenience of registering a // PeriodicChecker from an arbitrary func() error. func (registry *Registry) RegisterPeriodicThresholdFunc(name string, period time.Duration, threshold int, check CheckFunc) { registry.Register(name, PeriodicThresholdChecker(CheckFunc(check), period, threshold)) } // RegisterPeriodicThresholdFunc allows the convenience of registering a // PeriodicChecker in the default registry from an arbitrary func() error. func RegisterPeriodicThresholdFunc(name string, period time.Duration, threshold int, check CheckFunc) { DefaultRegistry.RegisterPeriodicThresholdFunc(name, period, threshold, check) } // StatusHandler returns a JSON blob with all the currently registered Health Checks // and their corresponding status. // Returns 503 if any Error status exists, 200 otherwise func StatusHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { checks := CheckStatus() status := http.StatusOK // If there is an error, return 503 if len(checks) != 0 { status = http.StatusServiceUnavailable } statusResponse(w, r, status, checks) } else { http.NotFound(w, r) } } // Handler returns a handler that will return 503 response code if the health // checks have failed. If everything is okay with the health checks, the // handler will pass through to the provided handler. Use this handler to // disable a web application when the health checks fail. func Handler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { checks := CheckStatus() if len(checks) != 0 { errcode.ServeJSON(w, errcode.ErrorCodeUnavailable. WithDetail("health check failed: please see /debug/health")) return } handler.ServeHTTP(w, r) // pass through }) } // statusResponse completes the request with a response describing the health // of the service. func statusResponse(w http.ResponseWriter, r *http.Request, status int, checks map[string]string) { p, err := json.Marshal(checks) if err != nil { context.GetLogger(context.Background()).Errorf("error serializing health status: %v", err) p, err = json.Marshal(struct { ServerError string `json:"server_error"` }{ ServerError: "Could not parse error message", }) status = http.StatusInternalServerError if err != nil { context.GetLogger(context.Background()).Errorf("error serializing health status failure message: %v", err) return } } w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Length", fmt.Sprint(len(p))) w.WriteHeader(status) if _, err := w.Write(p); err != nil { context.GetLogger(context.Background()).Errorf("error writing health status response body: %v", err) } } // Registers global /debug/health api endpoint, creates default registry func init() { DefaultRegistry = NewRegistry() http.HandleFunc("/debug/health", StatusHandler) } docker-registry-2.6.2~ds1/health/health_test.go000066400000000000000000000056331313450123100215440ustar00rootroot00000000000000package health import ( "errors" "fmt" "net/http" "net/http/httptest" "testing" ) // TestReturns200IfThereAreNoChecks ensures that the result code of the health // endpoint is 200 if there are not currently registered checks. func TestReturns200IfThereAreNoChecks(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "https://fakeurl.com/debug/health", nil) if err != nil { t.Errorf("Failed to create request.") } StatusHandler(recorder, req) if recorder.Code != 200 { t.Errorf("Did not get a 200.") } } // TestReturns500IfThereAreErrorChecks ensures that the result code of the // health endpoint is 500 if there are health checks with errors func TestReturns503IfThereAreErrorChecks(t *testing.T) { recorder := httptest.NewRecorder() req, err := http.NewRequest("GET", "https://fakeurl.com/debug/health", nil) if err != nil { t.Errorf("Failed to create request.") } // Create a manual error Register("some_check", CheckFunc(func() error { return errors.New("This Check did not succeed") })) StatusHandler(recorder, req) if recorder.Code != 503 { t.Errorf("Did not get a 503.") } } // TestHealthHandler ensures that our handler implementation correct protects // the web application when things aren't so healthy. func TestHealthHandler(t *testing.T) { // clear out existing checks. DefaultRegistry = NewRegistry() // protect an http server handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) // wrap it in our health handler handler = Handler(handler) // use this swap check status updater := NewStatusUpdater() Register("test_check", updater) // now, create a test server server := httptest.NewServer(handler) checkUp := func(t *testing.T, message string) { resp, err := http.Get(server.URL) if err != nil { t.Fatalf("error getting success status: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { t.Fatalf("unexpected response code from server when %s: %d != %d", message, resp.StatusCode, http.StatusNoContent) } // NOTE(stevvooe): we really don't care about the body -- the format is // not standardized or supported, yet. } checkDown := func(t *testing.T, message string) { resp, err := http.Get(server.URL) if err != nil { t.Fatalf("error getting down status: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("unexpected response code from server when %s: %d != %d", message, resp.StatusCode, http.StatusServiceUnavailable) } } // server should be up checkUp(t, "initial health check") // now, we fail the health check updater.Update(fmt.Errorf("the server is now out of commission")) checkDown(t, "server should be down") // should be down // bring server back up updater.Update(nil) checkUp(t, "when server is back up") // now we should be back up. } docker-registry-2.6.2~ds1/manifest/000077500000000000000000000000001313450123100172435ustar00rootroot00000000000000docker-registry-2.6.2~ds1/manifest/doc.go000066400000000000000000000000211313450123100203300ustar00rootroot00000000000000package manifest docker-registry-2.6.2~ds1/manifest/manifestlist/000077500000000000000000000000001313450123100217455ustar00rootroot00000000000000docker-registry-2.6.2~ds1/manifest/manifestlist/manifestlist.go000066400000000000000000000113311313450123100247750ustar00rootroot00000000000000package manifestlist import ( "encoding/json" "errors" "fmt" "github.com/docker/distribution" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" ) // MediaTypeManifestList specifies the mediaType for manifest lists. const MediaTypeManifestList = "application/vnd.docker.distribution.manifest.list.v2+json" // SchemaVersion provides a pre-initialized version structure for this // packages version of the manifest. var SchemaVersion = manifest.Versioned{ SchemaVersion: 2, MediaType: MediaTypeManifestList, } func init() { manifestListFunc := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { m := new(DeserializedManifestList) err := m.UnmarshalJSON(b) if err != nil { return nil, distribution.Descriptor{}, err } dgst := digest.FromBytes(b) return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifestList}, err } err := distribution.RegisterManifestSchema(MediaTypeManifestList, manifestListFunc) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } } // PlatformSpec specifies a platform where a particular image manifest is // applicable. type PlatformSpec struct { // Architecture field specifies the CPU architecture, for example // `amd64` or `ppc64`. Architecture string `json:"architecture"` // OS specifies the operating system, for example `linux` or `windows`. OS string `json:"os"` // OSVersion is an optional field specifying the operating system // version, for example `10.0.10586`. OSVersion string `json:"os.version,omitempty"` // OSFeatures is an optional field specifying an array of strings, // each listing a required OS feature (for example on Windows `win32k`). OSFeatures []string `json:"os.features,omitempty"` // Variant is an optional field specifying a variant of the CPU, for // example `ppc64le` to specify a little-endian version of a PowerPC CPU. Variant string `json:"variant,omitempty"` // Features is an optional field specifying an array of strings, each // listing a required CPU feature (for example `sse4` or `aes`). Features []string `json:"features,omitempty"` } // A ManifestDescriptor references a platform-specific manifest. type ManifestDescriptor struct { distribution.Descriptor // Platform specifies which platform the manifest pointed to by the // descriptor runs on. Platform PlatformSpec `json:"platform"` } // ManifestList references manifests for various platforms. type ManifestList struct { manifest.Versioned // Config references the image configuration as a blob. Manifests []ManifestDescriptor `json:"manifests"` } // References returnes the distribution descriptors for the referenced image // manifests. func (m ManifestList) References() []distribution.Descriptor { dependencies := make([]distribution.Descriptor, len(m.Manifests)) for i := range m.Manifests { dependencies[i] = m.Manifests[i].Descriptor } return dependencies } // DeserializedManifestList wraps ManifestList with a copy of the original // JSON. type DeserializedManifestList struct { ManifestList // canonical is the canonical byte representation of the Manifest. canonical []byte } // FromDescriptors takes a slice of descriptors, and returns a // DeserializedManifestList which contains the resulting manifest list // and its JSON representation. func FromDescriptors(descriptors []ManifestDescriptor) (*DeserializedManifestList, error) { m := ManifestList{ Versioned: SchemaVersion, } m.Manifests = make([]ManifestDescriptor, len(descriptors), len(descriptors)) copy(m.Manifests, descriptors) deserialized := DeserializedManifestList{ ManifestList: m, } var err error deserialized.canonical, err = json.MarshalIndent(&m, "", " ") return &deserialized, err } // UnmarshalJSON populates a new ManifestList struct from JSON data. func (m *DeserializedManifestList) UnmarshalJSON(b []byte) error { m.canonical = make([]byte, len(b), len(b)) // store manifest list in canonical copy(m.canonical, b) // Unmarshal canonical JSON into ManifestList object var manifestList ManifestList if err := json.Unmarshal(m.canonical, &manifestList); err != nil { return err } m.ManifestList = manifestList return nil } // MarshalJSON returns the contents of canonical. If canonical is empty, // marshals the inner contents. func (m *DeserializedManifestList) MarshalJSON() ([]byte, error) { if len(m.canonical) > 0 { return m.canonical, nil } return nil, errors.New("JSON representation not initialized in DeserializedManifestList") } // Payload returns the raw content of the manifest list. The contents can be // used to calculate the content identifier. func (m DeserializedManifestList) Payload() (string, []byte, error) { return m.MediaType, m.canonical, nil } docker-registry-2.6.2~ds1/manifest/manifestlist/manifestlist_test.go000066400000000000000000000063371313450123100260460ustar00rootroot00000000000000package manifestlist import ( "bytes" "encoding/json" "reflect" "testing" "github.com/docker/distribution" ) var expectedManifestListSerialization = []byte(`{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", "manifests": [ { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 985, "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", "platform": { "architecture": "amd64", "os": "linux", "features": [ "sse4" ] } }, { "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "size": 2392, "digest": "sha256:6346340964309634683409684360934680934608934608934608934068934608", "platform": { "architecture": "sun4m", "os": "sunos" } } ] }`) func TestManifestList(t *testing.T) { manifestDescriptors := []ManifestDescriptor{ { Descriptor: distribution.Descriptor{ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Size: 985, MediaType: "application/vnd.docker.distribution.manifest.v2+json", }, Platform: PlatformSpec{ Architecture: "amd64", OS: "linux", Features: []string{"sse4"}, }, }, { Descriptor: distribution.Descriptor{ Digest: "sha256:6346340964309634683409684360934680934608934608934608934068934608", Size: 2392, MediaType: "application/vnd.docker.distribution.manifest.v2+json", }, Platform: PlatformSpec{ Architecture: "sun4m", OS: "sunos", }, }, } deserialized, err := FromDescriptors(manifestDescriptors) if err != nil { t.Fatalf("error creating DeserializedManifestList: %v", err) } mediaType, canonical, err := deserialized.Payload() if mediaType != MediaTypeManifestList { t.Fatalf("unexpected media type: %s", mediaType) } // Check that the canonical field is the same as json.MarshalIndent // with these parameters. p, err := json.MarshalIndent(&deserialized.ManifestList, "", " ") if err != nil { t.Fatalf("error marshaling manifest list: %v", err) } if !bytes.Equal(p, canonical) { t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p)) } // Check that the canonical field has the expected value. if !bytes.Equal(expectedManifestListSerialization, canonical) { t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedManifestListSerialization)) } var unmarshalled DeserializedManifestList if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil { t.Fatalf("error unmarshaling manifest: %v", err) } if !reflect.DeepEqual(&unmarshalled, deserialized) { t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized) } references := deserialized.References() if len(references) != 2 { t.Fatalf("unexpected number of references: %d", len(references)) } for i := range references { if !reflect.DeepEqual(references[i], manifestDescriptors[i].Descriptor) { t.Fatalf("unexpected value %d returned by References: %v", i, references[i]) } } } docker-registry-2.6.2~ds1/manifest/schema1/000077500000000000000000000000001313450123100205645ustar00rootroot00000000000000docker-registry-2.6.2~ds1/manifest/schema1/config_builder.go000066400000000000000000000206201313450123100240660ustar00rootroot00000000000000package schema1 import ( "crypto/sha512" "encoding/json" "errors" "fmt" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/reference" "github.com/docker/libtrust" ) type diffID digest.Digest // gzippedEmptyTar is a gzip-compressed version of an empty tar file // (1024 NULL bytes) var gzippedEmptyTar = []byte{ 31, 139, 8, 0, 0, 9, 110, 136, 0, 255, 98, 24, 5, 163, 96, 20, 140, 88, 0, 8, 0, 0, 255, 255, 46, 175, 181, 239, 0, 4, 0, 0, } // digestSHA256GzippedEmptyTar is the canonical sha256 digest of // gzippedEmptyTar const digestSHA256GzippedEmptyTar = digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4") // configManifestBuilder is a type for constructing manifests from an image // configuration and generic descriptors. type configManifestBuilder struct { // bs is a BlobService used to create empty layer tars in the // blob store if necessary. bs distribution.BlobService // pk is the libtrust private key used to sign the final manifest. pk libtrust.PrivateKey // configJSON is configuration supplied when the ManifestBuilder was // created. configJSON []byte // ref contains the name and optional tag provided to NewConfigManifestBuilder. ref reference.Named // descriptors is the set of descriptors referencing the layers. descriptors []distribution.Descriptor // emptyTarDigest is set to a valid digest if an empty tar has been // put in the blob store; otherwise it is empty. emptyTarDigest digest.Digest } // NewConfigManifestBuilder is used to build new manifests for the current // schema version from an image configuration and a set of descriptors. // It takes a BlobService so that it can add an empty tar to the blob store // if the resulting manifest needs empty layers. func NewConfigManifestBuilder(bs distribution.BlobService, pk libtrust.PrivateKey, ref reference.Named, configJSON []byte) distribution.ManifestBuilder { return &configManifestBuilder{ bs: bs, pk: pk, configJSON: configJSON, ref: ref, } } // Build produces a final manifest from the given references func (mb *configManifestBuilder) Build(ctx context.Context) (m distribution.Manifest, err error) { type imageRootFS struct { Type string `json:"type"` DiffIDs []diffID `json:"diff_ids,omitempty"` BaseLayer string `json:"base_layer,omitempty"` } type imageHistory struct { Created time.Time `json:"created"` Author string `json:"author,omitempty"` CreatedBy string `json:"created_by,omitempty"` Comment string `json:"comment,omitempty"` EmptyLayer bool `json:"empty_layer,omitempty"` } type imageConfig struct { RootFS *imageRootFS `json:"rootfs,omitempty"` History []imageHistory `json:"history,omitempty"` Architecture string `json:"architecture,omitempty"` } var img imageConfig if err := json.Unmarshal(mb.configJSON, &img); err != nil { return nil, err } if len(img.History) == 0 { return nil, errors.New("empty history when trying to create schema1 manifest") } if len(img.RootFS.DiffIDs) != len(mb.descriptors) { return nil, fmt.Errorf("number of descriptors and number of layers in rootfs must match: len(%v) != len(%v)", img.RootFS.DiffIDs, mb.descriptors) } // Generate IDs for each layer // For non-top-level layers, create fake V1Compatibility strings that // fit the format and don't collide with anything else, but don't // result in runnable images on their own. type v1Compatibility struct { ID string `json:"id"` Parent string `json:"parent,omitempty"` Comment string `json:"comment,omitempty"` Created time.Time `json:"created"` ContainerConfig struct { Cmd []string } `json:"container_config,omitempty"` Author string `json:"author,omitempty"` ThrowAway bool `json:"throwaway,omitempty"` } fsLayerList := make([]FSLayer, len(img.History)) history := make([]History, len(img.History)) parent := "" layerCounter := 0 for i, h := range img.History[:len(img.History)-1] { var blobsum digest.Digest if h.EmptyLayer { if blobsum, err = mb.emptyTar(ctx); err != nil { return nil, err } } else { if len(img.RootFS.DiffIDs) <= layerCounter { return nil, errors.New("too many non-empty layers in History section") } blobsum = mb.descriptors[layerCounter].Digest layerCounter++ } v1ID := digest.FromBytes([]byte(blobsum.Hex() + " " + parent)).Hex() if i == 0 && img.RootFS.BaseLayer != "" { // windows-only baselayer setup baseID := sha512.Sum384([]byte(img.RootFS.BaseLayer)) parent = fmt.Sprintf("%x", baseID[:32]) } v1Compatibility := v1Compatibility{ ID: v1ID, Parent: parent, Comment: h.Comment, Created: h.Created, Author: h.Author, } v1Compatibility.ContainerConfig.Cmd = []string{img.History[i].CreatedBy} if h.EmptyLayer { v1Compatibility.ThrowAway = true } jsonBytes, err := json.Marshal(&v1Compatibility) if err != nil { return nil, err } reversedIndex := len(img.History) - i - 1 history[reversedIndex].V1Compatibility = string(jsonBytes) fsLayerList[reversedIndex] = FSLayer{BlobSum: blobsum} parent = v1ID } latestHistory := img.History[len(img.History)-1] var blobsum digest.Digest if latestHistory.EmptyLayer { if blobsum, err = mb.emptyTar(ctx); err != nil { return nil, err } } else { if len(img.RootFS.DiffIDs) <= layerCounter { return nil, errors.New("too many non-empty layers in History section") } blobsum = mb.descriptors[layerCounter].Digest } fsLayerList[0] = FSLayer{BlobSum: blobsum} dgst := digest.FromBytes([]byte(blobsum.Hex() + " " + parent + " " + string(mb.configJSON))) // Top-level v1compatibility string should be a modified version of the // image config. transformedConfig, err := MakeV1ConfigFromConfig(mb.configJSON, dgst.Hex(), parent, latestHistory.EmptyLayer) if err != nil { return nil, err } history[0].V1Compatibility = string(transformedConfig) tag := "" if tagged, isTagged := mb.ref.(reference.Tagged); isTagged { tag = tagged.Tag() } mfst := Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: mb.ref.Name(), Tag: tag, Architecture: img.Architecture, FSLayers: fsLayerList, History: history, } return Sign(&mfst, mb.pk) } // emptyTar pushes a compressed empty tar to the blob store if one doesn't // already exist, and returns its blobsum. func (mb *configManifestBuilder) emptyTar(ctx context.Context) (digest.Digest, error) { if mb.emptyTarDigest != "" { // Already put an empty tar return mb.emptyTarDigest, nil } descriptor, err := mb.bs.Stat(ctx, digestSHA256GzippedEmptyTar) switch err { case nil: mb.emptyTarDigest = descriptor.Digest return descriptor.Digest, nil case distribution.ErrBlobUnknown: // nop default: return "", err } // Add gzipped empty tar to the blob store descriptor, err = mb.bs.Put(ctx, "", gzippedEmptyTar) if err != nil { return "", err } mb.emptyTarDigest = descriptor.Digest return descriptor.Digest, nil } // AppendReference adds a reference to the current ManifestBuilder func (mb *configManifestBuilder) AppendReference(d distribution.Describable) error { // todo: verification here? mb.descriptors = append(mb.descriptors, d.Descriptor()) return nil } // References returns the current references added to this builder func (mb *configManifestBuilder) References() []distribution.Descriptor { return mb.descriptors } // MakeV1ConfigFromConfig creates an legacy V1 image config from image config JSON func MakeV1ConfigFromConfig(configJSON []byte, v1ID, parentV1ID string, throwaway bool) ([]byte, error) { // Top-level v1compatibility string should be a modified version of the // image config. var configAsMap map[string]*json.RawMessage if err := json.Unmarshal(configJSON, &configAsMap); err != nil { return nil, err } // Delete fields that didn't exist in old manifest delete(configAsMap, "rootfs") delete(configAsMap, "history") configAsMap["id"] = rawJSON(v1ID) if parentV1ID != "" { configAsMap["parent"] = rawJSON(parentV1ID) } if throwaway { configAsMap["throwaway"] = rawJSON(true) } return json.Marshal(configAsMap) } func rawJSON(value interface{}) *json.RawMessage { jsonval, err := json.Marshal(value) if err != nil { return nil } return (*json.RawMessage)(&jsonval) } docker-registry-2.6.2~ds1/manifest/schema1/config_builder_test.go000066400000000000000000000261231313450123100251310ustar00rootroot00000000000000package schema1 import ( "bytes" "compress/gzip" "io" "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/libtrust" ) type mockBlobService struct { descriptors map[digest.Digest]distribution.Descriptor } func (bs *mockBlobService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if descriptor, ok := bs.descriptors[dgst]; ok { return descriptor, nil } return distribution.Descriptor{}, distribution.ErrBlobUnknown } func (bs *mockBlobService) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { panic("not implemented") } func (bs *mockBlobService) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { panic("not implemented") } func (bs *mockBlobService) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { d := distribution.Descriptor{ Digest: digest.FromBytes(p), Size: int64(len(p)), MediaType: mediaType, } bs.descriptors[d.Digest] = d return d, nil } func (bs *mockBlobService) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { panic("not implemented") } func (bs *mockBlobService) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { panic("not implemented") } func TestEmptyTar(t *testing.T) { // Confirm that gzippedEmptyTar expands to 1024 NULL bytes. var decompressed [2048]byte gzipReader, err := gzip.NewReader(bytes.NewReader(gzippedEmptyTar)) if err != nil { t.Fatalf("NewReader returned error: %v", err) } n, err := gzipReader.Read(decompressed[:]) if n != 1024 { t.Fatalf("read returned %d bytes; expected 1024", n) } n, err = gzipReader.Read(decompressed[1024:]) if n != 0 { t.Fatalf("read returned %d bytes; expected 0", n) } if err != io.EOF { t.Fatal("read did not return io.EOF") } gzipReader.Close() for _, b := range decompressed[:1024] { if b != 0 { t.Fatal("nonzero byte in decompressed tar") } } // Confirm that digestSHA256EmptyTar is the digest of gzippedEmptyTar. dgst := digest.FromBytes(gzippedEmptyTar) if dgst != digestSHA256GzippedEmptyTar { t.Fatalf("digest mismatch for empty tar: expected %s got %s", digestSHA256GzippedEmptyTar, dgst) } } func TestConfigBuilder(t *testing.T) { imgJSON := `{ "architecture": "amd64", "config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": [ "/bin/sh", "-c", "echo hi" ], "Domainname": "", "Entrypoint": null, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "derived=true", "asdf=true" ], "Hostname": "23304fc829f9", "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", "Labels": {}, "OnBuild": [], "OpenStdin": false, "StdinOnce": false, "Tty": false, "User": "", "Volumes": null, "WorkingDir": "" }, "container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001", "container_config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": [ "/bin/sh", "-c", "#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]" ], "Domainname": "", "Entrypoint": null, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "derived=true", "asdf=true" ], "Hostname": "23304fc829f9", "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", "Labels": {}, "OnBuild": [], "OpenStdin": false, "StdinOnce": false, "Tty": false, "User": "", "Volumes": null, "WorkingDir": "" }, "created": "2015-11-04T23:06:32.365666163Z", "docker_version": "1.9.0-dev", "history": [ { "created": "2015-10-31T22:22:54.690851953Z", "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" }, { "created": "2015-10-31T22:22:55.613815829Z", "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" }, { "created": "2015-11-04T23:06:30.934316144Z", "created_by": "/bin/sh -c #(nop) ENV derived=true", "empty_layer": true }, { "created": "2015-11-04T23:06:31.192097572Z", "created_by": "/bin/sh -c #(nop) ENV asdf=true", "empty_layer": true }, { "author": "Alyssa P. Hacker \u003calyspdev@example.com\u003e", "created": "2015-11-04T23:06:32.083868454Z", "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024" }, { "created": "2015-11-04T23:06:32.365666163Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]", "empty_layer": true } ], "os": "linux", "rootfs": { "diff_ids": [ "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", "sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49" ], "type": "layers" } }` descriptors := []distribution.Descriptor{ {Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, {Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, {Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("could not generate key for testing: %v", err) } bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} ref, err := reference.ParseNamed("testrepo:testtag") if err != nil { t.Fatalf("could not parse reference: %v", err) } builder := NewConfigManifestBuilder(bs, pk, ref, []byte(imgJSON)) for _, d := range descriptors { if err := builder.AppendReference(d); err != nil { t.Fatalf("AppendReference returned error: %v", err) } } signed, err := builder.Build(context.Background()) if err != nil { t.Fatalf("Build returned error: %v", err) } // Check that the gzipped empty layer tar was put in the blob store _, err = bs.Stat(context.Background(), digestSHA256GzippedEmptyTar) if err != nil { t.Fatal("gzipped empty tar was not put in the blob store") } manifest := signed.(*SignedManifest).Manifest if manifest.Versioned.SchemaVersion != 1 { t.Fatal("SchemaVersion != 1") } if manifest.Name != "testrepo" { t.Fatal("incorrect name in manifest") } if manifest.Tag != "testtag" { t.Fatal("incorrect tag in manifest") } if manifest.Architecture != "amd64" { t.Fatal("incorrect arch in manifest") } expectedFSLayers := []FSLayer{ {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, {BlobSum: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, {BlobSum: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa")}, {BlobSum: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4")}, } if len(manifest.FSLayers) != len(expectedFSLayers) { t.Fatalf("wrong number of FSLayers: %d", len(manifest.FSLayers)) } if !reflect.DeepEqual(manifest.FSLayers, expectedFSLayers) { t.Fatal("wrong FSLayers list") } expectedV1Compatibility := []string{ `{"architecture":"amd64","config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","echo hi"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"container":"e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001","container_config":{"AttachStderr":false,"AttachStdin":false,"AttachStdout":false,"Cmd":["/bin/sh","-c","#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]"],"Domainname":"","Entrypoint":null,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","derived=true","asdf=true"],"Hostname":"23304fc829f9","Image":"sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246","Labels":{},"OnBuild":[],"OpenStdin":false,"StdinOnce":false,"Tty":false,"User":"","Volumes":null,"WorkingDir":""},"created":"2015-11-04T23:06:32.365666163Z","docker_version":"1.9.0-dev","id":"69e5c1bfadad697fdb6db59f6326648fa119e0c031a0eda33b8cfadcab54ba7f","os":"linux","parent":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","throwaway":true}`, `{"id":"74cf9c92699240efdba1903c2748ef57105d5bedc588084c4e88f3bb1c3ef0b0","parent":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","created":"2015-11-04T23:06:32.083868454Z","container_config":{"Cmd":["/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024"]},"author":"Alyssa P. Hacker \u003calyspdev@example.com\u003e"}`, `{"id":"178be37afc7c49e951abd75525dbe0871b62ad49402f037164ee6314f754599d","parent":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","created":"2015-11-04T23:06:31.192097572Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV asdf=true"]},"throwaway":true}`, `{"id":"b449305a55a283538c4574856a8b701f2a3d5ec08ef8aec47f385f20339a4866","parent":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","created":"2015-11-04T23:06:30.934316144Z","container_config":{"Cmd":["/bin/sh -c #(nop) ENV derived=true"]},"throwaway":true}`, `{"id":"9e3447ca24cb96d86ebd5960cb34d1299b07e0a0e03801d90b9969a2c187dd6e","parent":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:55.613815829Z","container_config":{"Cmd":["/bin/sh -c #(nop) CMD [\"sh\"]"]}}`, `{"id":"3690474eb5b4b26fdfbd89c6e159e8cc376ca76ef48032a30fa6aafd56337880","created":"2015-10-31T22:22:54.690851953Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /"]}}`, } if len(manifest.History) != len(expectedV1Compatibility) { t.Fatalf("wrong number of history entries: %d", len(manifest.History)) } for i := range expectedV1Compatibility { if manifest.History[i].V1Compatibility != expectedV1Compatibility[i] { t.Errorf("wrong V1Compatibility %d. expected:\n%s\ngot:\n%s", i, expectedV1Compatibility[i], manifest.History[i].V1Compatibility) } } } docker-registry-2.6.2~ds1/manifest/schema1/manifest.go000066400000000000000000000127561313450123100227340ustar00rootroot00000000000000package schema1 import ( "encoding/json" "fmt" "github.com/docker/distribution" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/libtrust" ) const ( // MediaTypeManifest specifies the mediaType for the current version. Note // that for schema version 1, the the media is optionally "application/json". MediaTypeManifest = "application/vnd.docker.distribution.manifest.v1+json" // MediaTypeSignedManifest specifies the mediatype for current SignedManifest version MediaTypeSignedManifest = "application/vnd.docker.distribution.manifest.v1+prettyjws" // MediaTypeManifestLayer specifies the media type for manifest layers MediaTypeManifestLayer = "application/vnd.docker.container.image.rootfs.diff+x-gtar" ) var ( // SchemaVersion provides a pre-initialized version structure for this // packages version of the manifest. SchemaVersion = manifest.Versioned{ SchemaVersion: 1, } ) func init() { schema1Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { sm := new(SignedManifest) err := sm.UnmarshalJSON(b) if err != nil { return nil, distribution.Descriptor{}, err } desc := distribution.Descriptor{ Digest: digest.FromBytes(sm.Canonical), Size: int64(len(sm.Canonical)), MediaType: MediaTypeSignedManifest, } return sm, desc, err } err := distribution.RegisterManifestSchema(MediaTypeSignedManifest, schema1Func) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } err = distribution.RegisterManifestSchema("", schema1Func) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } err = distribution.RegisterManifestSchema("application/json", schema1Func) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } } // FSLayer is a container struct for BlobSums defined in an image manifest type FSLayer struct { // BlobSum is the tarsum of the referenced filesystem image layer BlobSum digest.Digest `json:"blobSum"` } // History stores unstructured v1 compatibility information type History struct { // V1Compatibility is the raw v1 compatibility information V1Compatibility string `json:"v1Compatibility"` } // Manifest provides the base accessible fields for working with V2 image // format in the registry. type Manifest struct { manifest.Versioned // Name is the name of the image's repository Name string `json:"name"` // Tag is the tag of the image specified by this manifest Tag string `json:"tag"` // Architecture is the host architecture on which this image is intended to // run Architecture string `json:"architecture"` // FSLayers is a list of filesystem layer blobSums contained in this image FSLayers []FSLayer `json:"fsLayers"` // History is a list of unstructured historical data for v1 compatibility History []History `json:"history"` } // SignedManifest provides an envelope for a signed image manifest, including // the format sensitive raw bytes. type SignedManifest struct { Manifest // Canonical is the canonical byte representation of the ImageManifest, // without any attached signatures. The manifest byte // representation cannot change or it will have to be re-signed. Canonical []byte `json:"-"` // all contains the byte representation of the Manifest including signatures // and is returned by Payload() all []byte } // UnmarshalJSON populates a new SignedManifest struct from JSON data. func (sm *SignedManifest) UnmarshalJSON(b []byte) error { sm.all = make([]byte, len(b), len(b)) // store manifest and signatures in all copy(sm.all, b) jsig, err := libtrust.ParsePrettySignature(b, "signatures") if err != nil { return err } // Resolve the payload in the manifest. bytes, err := jsig.Payload() if err != nil { return err } // sm.Canonical stores the canonical manifest JSON sm.Canonical = make([]byte, len(bytes), len(bytes)) copy(sm.Canonical, bytes) // Unmarshal canonical JSON into Manifest object var manifest Manifest if err := json.Unmarshal(sm.Canonical, &manifest); err != nil { return err } sm.Manifest = manifest return nil } // References returnes the descriptors of this manifests references func (sm SignedManifest) References() []distribution.Descriptor { dependencies := make([]distribution.Descriptor, len(sm.FSLayers)) for i, fsLayer := range sm.FSLayers { dependencies[i] = distribution.Descriptor{ MediaType: "application/vnd.docker.container.image.rootfs.diff+x-gtar", Digest: fsLayer.BlobSum, } } return dependencies } // MarshalJSON returns the contents of raw. If Raw is nil, marshals the inner // contents. Applications requiring a marshaled signed manifest should simply // use Raw directly, since the the content produced by json.Marshal will be // compacted and will fail signature checks. func (sm *SignedManifest) MarshalJSON() ([]byte, error) { if len(sm.all) > 0 { return sm.all, nil } // If the raw data is not available, just dump the inner content. return json.Marshal(&sm.Manifest) } // Payload returns the signed content of the signed manifest. func (sm SignedManifest) Payload() (string, []byte, error) { return MediaTypeSignedManifest, sm.all, nil } // Signatures returns the signatures as provided by // (*libtrust.JSONSignature).Signatures. The byte slices are opaque jws // signatures. func (sm *SignedManifest) Signatures() ([][]byte, error) { jsig, err := libtrust.ParsePrettySignature(sm.all, "signatures") if err != nil { return nil, err } // Resolve the payload in the manifest. return jsig.Signatures() } docker-registry-2.6.2~ds1/manifest/schema1/manifest_test.go000066400000000000000000000052361313450123100237660ustar00rootroot00000000000000package schema1 import ( "bytes" "encoding/json" "reflect" "testing" "github.com/docker/libtrust" ) type testEnv struct { name, tag string invalidSigned *SignedManifest signed *SignedManifest pk libtrust.PrivateKey } func TestManifestMarshaling(t *testing.T) { env := genEnv(t) // Check that the all field is the same as json.MarshalIndent with these // parameters. p, err := json.MarshalIndent(env.signed, "", " ") if err != nil { t.Fatalf("error marshaling manifest: %v", err) } if !bytes.Equal(p, env.signed.all) { t.Fatalf("manifest bytes not equal: %q != %q", string(env.signed.all), string(p)) } } func TestManifestUnmarshaling(t *testing.T) { env := genEnv(t) var signed SignedManifest if err := json.Unmarshal(env.signed.all, &signed); err != nil { t.Fatalf("error unmarshaling signed manifest: %v", err) } if !reflect.DeepEqual(&signed, env.signed) { t.Fatalf("manifests are different after unmarshaling: %v != %v", signed, env.signed) } } func TestManifestVerification(t *testing.T) { env := genEnv(t) publicKeys, err := Verify(env.signed) if err != nil { t.Fatalf("error verifying manifest: %v", err) } if len(publicKeys) == 0 { t.Fatalf("no public keys found in signature") } var found bool publicKey := env.pk.PublicKey() // ensure that one of the extracted public keys matches the private key. for _, candidate := range publicKeys { if candidate.KeyID() == publicKey.KeyID() { found = true break } } if !found { t.Fatalf("expected public key, %v, not found in verified keys: %v", publicKey, publicKeys) } // Check that an invalid manifest fails verification _, err = Verify(env.invalidSigned) if err != nil { t.Fatalf("Invalid manifest should not pass Verify()") } } func genEnv(t *testing.T) *testEnv { pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("error generating test key: %v", err) } name, tag := "foo/bar", "test" invalid := Manifest{ Versioned: SchemaVersion, Name: name, Tag: tag, FSLayers: []FSLayer{ { BlobSum: "asdf", }, { BlobSum: "qwer", }, }, } valid := Manifest{ Versioned: SchemaVersion, Name: name, Tag: tag, FSLayers: []FSLayer{ { BlobSum: "asdf", }, }, History: []History{ { V1Compatibility: "", }, }, } sm, err := Sign(&valid, pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } invalidSigned, err := Sign(&invalid, pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } return &testEnv{ name: name, tag: tag, invalidSigned: invalidSigned, signed: sm, pk: pk, } } docker-registry-2.6.2~ds1/manifest/schema1/reference_builder.go000066400000000000000000000054061313450123100245640ustar00rootroot00000000000000package schema1 import ( "fmt" "errors" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/reference" "github.com/docker/libtrust" ) // referenceManifestBuilder is a type for constructing manifests from schema1 // dependencies. type referenceManifestBuilder struct { Manifest pk libtrust.PrivateKey } // NewReferenceManifestBuilder is used to build new manifests for the current // schema version using schema1 dependencies. func NewReferenceManifestBuilder(pk libtrust.PrivateKey, ref reference.Named, architecture string) distribution.ManifestBuilder { tag := "" if tagged, isTagged := ref.(reference.Tagged); isTagged { tag = tagged.Tag() } return &referenceManifestBuilder{ Manifest: Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: ref.Name(), Tag: tag, Architecture: architecture, }, pk: pk, } } func (mb *referenceManifestBuilder) Build(ctx context.Context) (distribution.Manifest, error) { m := mb.Manifest if len(m.FSLayers) == 0 { return nil, errors.New("cannot build manifest with zero layers or history") } m.FSLayers = make([]FSLayer, len(mb.Manifest.FSLayers)) m.History = make([]History, len(mb.Manifest.History)) copy(m.FSLayers, mb.Manifest.FSLayers) copy(m.History, mb.Manifest.History) return Sign(&m, mb.pk) } // AppendReference adds a reference to the current ManifestBuilder func (mb *referenceManifestBuilder) AppendReference(d distribution.Describable) error { r, ok := d.(Reference) if !ok { return fmt.Errorf("Unable to add non-reference type to v1 builder") } // Entries need to be prepended mb.Manifest.FSLayers = append([]FSLayer{{BlobSum: r.Digest}}, mb.Manifest.FSLayers...) mb.Manifest.History = append([]History{r.History}, mb.Manifest.History...) return nil } // References returns the current references added to this builder func (mb *referenceManifestBuilder) References() []distribution.Descriptor { refs := make([]distribution.Descriptor, len(mb.Manifest.FSLayers)) for i := range mb.Manifest.FSLayers { layerDigest := mb.Manifest.FSLayers[i].BlobSum history := mb.Manifest.History[i] ref := Reference{layerDigest, 0, history} refs[i] = ref.Descriptor() } return refs } // Reference describes a manifest v2, schema version 1 dependency. // An FSLayer associated with a history entry. type Reference struct { Digest digest.Digest Size int64 // if we know it, set it for the descriptor. History History } // Descriptor describes a reference func (r Reference) Descriptor() distribution.Descriptor { return distribution.Descriptor{ MediaType: MediaTypeManifestLayer, Digest: r.Digest, Size: r.Size, } } docker-registry-2.6.2~ds1/manifest/schema1/reference_builder_test.go000066400000000000000000000050151313450123100256170ustar00rootroot00000000000000package schema1 import ( "testing" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/reference" "github.com/docker/libtrust" ) func makeSignedManifest(t *testing.T, pk libtrust.PrivateKey, refs []Reference) *SignedManifest { u := &Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: "foo/bar", Tag: "latest", Architecture: "amd64", } for i := len(refs) - 1; i >= 0; i-- { u.FSLayers = append(u.FSLayers, FSLayer{ BlobSum: refs[i].Digest, }) u.History = append(u.History, History{ V1Compatibility: refs[i].History.V1Compatibility, }) } signedManifest, err := Sign(u, pk) if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } return signedManifest } func TestReferenceBuilder(t *testing.T) { pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } r1 := Reference{ Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 1, History: History{V1Compatibility: "{\"a\" : 1 }"}, } r2 := Reference{ Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Size: 2, History: History{V1Compatibility: "{\"\a\" : 2 }"}, } handCrafted := makeSignedManifest(t, pk, []Reference{r1, r2}) ref, err := reference.ParseNamed(handCrafted.Manifest.Name) if err != nil { t.Fatalf("could not parse reference: %v", err) } ref, err = reference.WithTag(ref, handCrafted.Manifest.Tag) if err != nil { t.Fatalf("could not add tag: %v", err) } b := NewReferenceManifestBuilder(pk, ref, handCrafted.Manifest.Architecture) _, err = b.Build(context.Background()) if err == nil { t.Fatal("Expected error building zero length manifest") } err = b.AppendReference(r1) if err != nil { t.Fatal(err) } err = b.AppendReference(r2) if err != nil { t.Fatal(err) } refs := b.References() if len(refs) != 2 { t.Fatalf("Unexpected reference count : %d != %d", 2, len(refs)) } // Ensure ordering if refs[0].Digest != r2.Digest { t.Fatalf("Unexpected reference : %v", refs[0]) } m, err := b.Build(context.Background()) if err != nil { t.Fatal(err) } built, ok := m.(*SignedManifest) if !ok { t.Fatalf("unexpected type from Build() : %T", built) } d1 := digest.FromBytes(built.Canonical) d2 := digest.FromBytes(handCrafted.Canonical) if d1 != d2 { t.Errorf("mismatching canonical JSON") } } docker-registry-2.6.2~ds1/manifest/schema1/sign.go000066400000000000000000000026451313450123100220620ustar00rootroot00000000000000package schema1 import ( "crypto/x509" "encoding/json" "github.com/docker/libtrust" ) // Sign signs the manifest with the provided private key, returning a // SignedManifest. This typically won't be used within the registry, except // for testing. func Sign(m *Manifest, pk libtrust.PrivateKey) (*SignedManifest, error) { p, err := json.MarshalIndent(m, "", " ") if err != nil { return nil, err } js, err := libtrust.NewJSONSignature(p) if err != nil { return nil, err } if err := js.Sign(pk); err != nil { return nil, err } pretty, err := js.PrettySignature("signatures") if err != nil { return nil, err } return &SignedManifest{ Manifest: *m, all: pretty, Canonical: p, }, nil } // SignWithChain signs the manifest with the given private key and x509 chain. // The public key of the first element in the chain must be the public key // corresponding with the sign key. func SignWithChain(m *Manifest, key libtrust.PrivateKey, chain []*x509.Certificate) (*SignedManifest, error) { p, err := json.MarshalIndent(m, "", " ") if err != nil { return nil, err } js, err := libtrust.NewJSONSignature(p) if err != nil { return nil, err } if err := js.SignWithChain(key, chain); err != nil { return nil, err } pretty, err := js.PrettySignature("signatures") if err != nil { return nil, err } return &SignedManifest{ Manifest: *m, all: pretty, Canonical: p, }, nil } docker-registry-2.6.2~ds1/manifest/schema1/verify.go000066400000000000000000000015541313450123100224240ustar00rootroot00000000000000package schema1 import ( "crypto/x509" "github.com/Sirupsen/logrus" "github.com/docker/libtrust" ) // Verify verifies the signature of the signed manifest returning the public // keys used during signing. func Verify(sm *SignedManifest) ([]libtrust.PublicKey, error) { js, err := libtrust.ParsePrettySignature(sm.all, "signatures") if err != nil { logrus.WithField("err", err).Debugf("(*SignedManifest).Verify") return nil, err } return js.Verify() } // VerifyChains verifies the signature of the signed manifest against the // certificate pool returning the list of verified chains. Signatures without // an x509 chain are not checked. func VerifyChains(sm *SignedManifest, ca *x509.CertPool) ([][]*x509.Certificate, error) { js, err := libtrust.ParsePrettySignature(sm.all, "signatures") if err != nil { return nil, err } return js.VerifyChains(ca) } docker-registry-2.6.2~ds1/manifest/schema2/000077500000000000000000000000001313450123100205655ustar00rootroot00000000000000docker-registry-2.6.2~ds1/manifest/schema2/builder.go000066400000000000000000000043701313450123100225460ustar00rootroot00000000000000package schema2 import ( "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" ) // builder is a type for constructing manifests. type builder struct { // bs is a BlobService used to publish the configuration blob. bs distribution.BlobService // configJSON references configJSON []byte // layers is a list of layer descriptors that gets built by successive // calls to AppendReference. layers []distribution.Descriptor } // NewManifestBuilder is used to build new manifests for the current schema // version. It takes a BlobService so it can publish the configuration blob // as part of the Build process. func NewManifestBuilder(bs distribution.BlobService, configJSON []byte) distribution.ManifestBuilder { mb := &builder{ bs: bs, configJSON: make([]byte, len(configJSON)), } copy(mb.configJSON, configJSON) return mb } // Build produces a final manifest from the given references. func (mb *builder) Build(ctx context.Context) (distribution.Manifest, error) { m := Manifest{ Versioned: SchemaVersion, Layers: make([]distribution.Descriptor, len(mb.layers)), } copy(m.Layers, mb.layers) configDigest := digest.FromBytes(mb.configJSON) var err error m.Config, err = mb.bs.Stat(ctx, configDigest) switch err { case nil: // Override MediaType, since Put always replaces the specified media // type with application/octet-stream in the descriptor it returns. m.Config.MediaType = MediaTypeConfig return FromStruct(m) case distribution.ErrBlobUnknown: // nop default: return nil, err } // Add config to the blob store m.Config, err = mb.bs.Put(ctx, MediaTypeConfig, mb.configJSON) // Override MediaType, since Put always replaces the specified media // type with application/octet-stream in the descriptor it returns. m.Config.MediaType = MediaTypeConfig if err != nil { return nil, err } return FromStruct(m) } // AppendReference adds a reference to the current ManifestBuilder. func (mb *builder) AppendReference(d distribution.Describable) error { mb.layers = append(mb.layers, d.Descriptor()) return nil } // References returns the current references added to this builder. func (mb *builder) References() []distribution.Descriptor { return mb.layers } docker-registry-2.6.2~ds1/manifest/schema2/builder_test.go000066400000000000000000000144361313450123100236110ustar00rootroot00000000000000package schema2 import ( "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" ) type mockBlobService struct { descriptors map[digest.Digest]distribution.Descriptor } func (bs *mockBlobService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if descriptor, ok := bs.descriptors[dgst]; ok { return descriptor, nil } return distribution.Descriptor{}, distribution.ErrBlobUnknown } func (bs *mockBlobService) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { panic("not implemented") } func (bs *mockBlobService) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { panic("not implemented") } func (bs *mockBlobService) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { d := distribution.Descriptor{ Digest: digest.FromBytes(p), Size: int64(len(p)), MediaType: "application/octet-stream", } bs.descriptors[d.Digest] = d return d, nil } func (bs *mockBlobService) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { panic("not implemented") } func (bs *mockBlobService) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { panic("not implemented") } func TestBuilder(t *testing.T) { imgJSON := []byte(`{ "architecture": "amd64", "config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": [ "/bin/sh", "-c", "echo hi" ], "Domainname": "", "Entrypoint": null, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "derived=true", "asdf=true" ], "Hostname": "23304fc829f9", "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", "Labels": {}, "OnBuild": [], "OpenStdin": false, "StdinOnce": false, "Tty": false, "User": "", "Volumes": null, "WorkingDir": "" }, "container": "e91032eb0403a61bfe085ff5a5a48e3659e5a6deae9f4d678daa2ae399d5a001", "container_config": { "AttachStderr": false, "AttachStdin": false, "AttachStdout": false, "Cmd": [ "/bin/sh", "-c", "#(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]" ], "Domainname": "", "Entrypoint": null, "Env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "derived=true", "asdf=true" ], "Hostname": "23304fc829f9", "Image": "sha256:4ab15c48b859c2920dd5224f92aabcd39a52794c5b3cf088fb3bbb438756c246", "Labels": {}, "OnBuild": [], "OpenStdin": false, "StdinOnce": false, "Tty": false, "User": "", "Volumes": null, "WorkingDir": "" }, "created": "2015-11-04T23:06:32.365666163Z", "docker_version": "1.9.0-dev", "history": [ { "created": "2015-10-31T22:22:54.690851953Z", "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" }, { "created": "2015-10-31T22:22:55.613815829Z", "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" }, { "created": "2015-11-04T23:06:30.934316144Z", "created_by": "/bin/sh -c #(nop) ENV derived=true", "empty_layer": true }, { "created": "2015-11-04T23:06:31.192097572Z", "created_by": "/bin/sh -c #(nop) ENV asdf=true", "empty_layer": true }, { "created": "2015-11-04T23:06:32.083868454Z", "created_by": "/bin/sh -c dd if=/dev/zero of=/file bs=1024 count=1024" }, { "created": "2015-11-04T23:06:32.365666163Z", "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\" \"-c\" \"echo hi\"]", "empty_layer": true } ], "os": "linux", "rootfs": { "diff_ids": [ "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef", "sha256:13f53e08df5a220ab6d13c58b2bf83a59cbdc2e04d0a3f041ddf4b0ba4112d49" ], "type": "layers" } }`) configDigest := digest.FromBytes(imgJSON) descriptors := []distribution.Descriptor{ { Digest: digest.Digest("sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), Size: 5312, MediaType: MediaTypeLayer, }, { Digest: digest.Digest("sha256:86e0e091d0da6bde2456dbb48306f3956bbeb2eae1b5b9a43045843f69fe4aaa"), Size: 235231, MediaType: MediaTypeLayer, }, { Digest: digest.Digest("sha256:b4ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"), Size: 639152, MediaType: MediaTypeLayer, }, } bs := &mockBlobService{descriptors: make(map[digest.Digest]distribution.Descriptor)} builder := NewManifestBuilder(bs, imgJSON) for _, d := range descriptors { if err := builder.AppendReference(d); err != nil { t.Fatalf("AppendReference returned error: %v", err) } } built, err := builder.Build(context.Background()) if err != nil { t.Fatalf("Build returned error: %v", err) } // Check that the config was put in the blob store _, err = bs.Stat(context.Background(), configDigest) if err != nil { t.Fatal("config was not put in the blob store") } manifest := built.(*DeserializedManifest).Manifest if manifest.Versioned.SchemaVersion != 2 { t.Fatal("SchemaVersion != 2") } target := manifest.Target() if target.Digest != configDigest { t.Fatalf("unexpected digest in target: %s", target.Digest.String()) } if target.MediaType != MediaTypeConfig { t.Fatalf("unexpected media type in target: %s", target.MediaType) } if target.Size != 3153 { t.Fatalf("unexpected size in target: %d", target.Size) } references := manifest.References() expected := append([]distribution.Descriptor{manifest.Target()}, descriptors...) if !reflect.DeepEqual(references, expected) { t.Fatal("References() does not match the descriptors added") } } docker-registry-2.6.2~ds1/manifest/schema2/manifest.go000066400000000000000000000077721313450123100227370ustar00rootroot00000000000000package schema2 import ( "encoding/json" "errors" "fmt" "github.com/docker/distribution" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" ) const ( // MediaTypeManifest specifies the mediaType for the current version. MediaTypeManifest = "application/vnd.docker.distribution.manifest.v2+json" // MediaTypeConfig specifies the mediaType for the image configuration. MediaTypeConfig = "application/vnd.docker.container.image.v1+json" // MediaTypePluginConfig specifies the mediaType for plugin configuration. MediaTypePluginConfig = "application/vnd.docker.plugin.v1+json" // MediaTypeLayer is the mediaType used for layers referenced by the // manifest. MediaTypeLayer = "application/vnd.docker.image.rootfs.diff.tar.gzip" // MediaTypeForeignLayer is the mediaType used for layers that must be // downloaded from foreign URLs. MediaTypeForeignLayer = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" ) var ( // SchemaVersion provides a pre-initialized version structure for this // packages version of the manifest. SchemaVersion = manifest.Versioned{ SchemaVersion: 2, MediaType: MediaTypeManifest, } ) func init() { schema2Func := func(b []byte) (distribution.Manifest, distribution.Descriptor, error) { m := new(DeserializedManifest) err := m.UnmarshalJSON(b) if err != nil { return nil, distribution.Descriptor{}, err } dgst := digest.FromBytes(b) return m, distribution.Descriptor{Digest: dgst, Size: int64(len(b)), MediaType: MediaTypeManifest}, err } err := distribution.RegisterManifestSchema(MediaTypeManifest, schema2Func) if err != nil { panic(fmt.Sprintf("Unable to register manifest: %s", err)) } } // Manifest defines a schema2 manifest. type Manifest struct { manifest.Versioned // Config references the image configuration as a blob. Config distribution.Descriptor `json:"config"` // Layers lists descriptors for the layers referenced by the // configuration. Layers []distribution.Descriptor `json:"layers"` } // References returnes the descriptors of this manifests references. func (m Manifest) References() []distribution.Descriptor { references := make([]distribution.Descriptor, 0, 1+len(m.Layers)) references = append(references, m.Config) references = append(references, m.Layers...) return references } // Target returns the target of this signed manifest. func (m Manifest) Target() distribution.Descriptor { return m.Config } // DeserializedManifest wraps Manifest with a copy of the original JSON. // It satisfies the distribution.Manifest interface. type DeserializedManifest struct { Manifest // canonical is the canonical byte representation of the Manifest. canonical []byte } // FromStruct takes a Manifest structure, marshals it to JSON, and returns a // DeserializedManifest which contains the manifest and its JSON representation. func FromStruct(m Manifest) (*DeserializedManifest, error) { var deserialized DeserializedManifest deserialized.Manifest = m var err error deserialized.canonical, err = json.MarshalIndent(&m, "", " ") return &deserialized, err } // UnmarshalJSON populates a new Manifest struct from JSON data. func (m *DeserializedManifest) UnmarshalJSON(b []byte) error { m.canonical = make([]byte, len(b), len(b)) // store manifest in canonical copy(m.canonical, b) // Unmarshal canonical JSON into Manifest object var manifest Manifest if err := json.Unmarshal(m.canonical, &manifest); err != nil { return err } m.Manifest = manifest return nil } // MarshalJSON returns the contents of canonical. If canonical is empty, // marshals the inner contents. func (m *DeserializedManifest) MarshalJSON() ([]byte, error) { if len(m.canonical) > 0 { return m.canonical, nil } return nil, errors.New("JSON representation not initialized in DeserializedManifest") } // Payload returns the raw content of the manifest. The contents can be used to // calculate the content identifier. func (m DeserializedManifest) Payload() (string, []byte, error) { return m.MediaType, m.canonical, nil } docker-registry-2.6.2~ds1/manifest/schema2/manifest_test.go000066400000000000000000000065511313450123100237700ustar00rootroot00000000000000package schema2 import ( "bytes" "encoding/json" "reflect" "testing" "github.com/docker/distribution" ) var expectedManifestSerialization = []byte(`{ "schemaVersion": 2, "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "mediaType": "application/vnd.docker.container.image.v1+json", "size": 985, "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" }, "layers": [ { "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", "size": 153263, "digest": "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" } ] }`) func TestManifest(t *testing.T) { manifest := Manifest{ Versioned: SchemaVersion, Config: distribution.Descriptor{ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Size: 985, MediaType: MediaTypeConfig, }, Layers: []distribution.Descriptor{ { Digest: "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b", Size: 153263, MediaType: MediaTypeLayer, }, }, } deserialized, err := FromStruct(manifest) if err != nil { t.Fatalf("error creating DeserializedManifest: %v", err) } mediaType, canonical, err := deserialized.Payload() if mediaType != MediaTypeManifest { t.Fatalf("unexpected media type: %s", mediaType) } // Check that the canonical field is the same as json.MarshalIndent // with these parameters. p, err := json.MarshalIndent(&manifest, "", " ") if err != nil { t.Fatalf("error marshaling manifest: %v", err) } if !bytes.Equal(p, canonical) { t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(p)) } // Check that canonical field matches expected value. if !bytes.Equal(expectedManifestSerialization, canonical) { t.Fatalf("manifest bytes not equal: %q != %q", string(canonical), string(expectedManifestSerialization)) } var unmarshalled DeserializedManifest if err := json.Unmarshal(deserialized.canonical, &unmarshalled); err != nil { t.Fatalf("error unmarshaling manifest: %v", err) } if !reflect.DeepEqual(&unmarshalled, deserialized) { t.Fatalf("manifests are different after unmarshaling: %v != %v", unmarshalled, *deserialized) } target := deserialized.Target() if target.Digest != "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" { t.Fatalf("unexpected digest in target: %s", target.Digest.String()) } if target.MediaType != MediaTypeConfig { t.Fatalf("unexpected media type in target: %s", target.MediaType) } if target.Size != 985 { t.Fatalf("unexpected size in target: %d", target.Size) } references := deserialized.References() if len(references) != 2 { t.Fatalf("unexpected number of references: %d", len(references)) } if !reflect.DeepEqual(references[0], target) { t.Fatalf("first reference should be target: %v != %v", references[0], target) } // Test the second reference if references[1].Digest != "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" { t.Fatalf("unexpected digest in reference: %s", references[0].Digest.String()) } if references[1].MediaType != MediaTypeLayer { t.Fatalf("unexpected media type in reference: %s", references[0].MediaType) } if references[1].Size != 153263 { t.Fatalf("unexpected size in reference: %d", references[0].Size) } } docker-registry-2.6.2~ds1/manifest/versioned.go000066400000000000000000000006671313450123100216010ustar00rootroot00000000000000package manifest // Versioned provides a struct with the manifest schemaVersion and mediaType. // Incoming content with unknown schema version can be decoded against this // struct to check the version. type Versioned struct { // SchemaVersion is the image manifest schema that this image follows SchemaVersion int `json:"schemaVersion"` // MediaType is the media type of this schema. MediaType string `json:"mediaType,omitempty"` } docker-registry-2.6.2~ds1/manifests.go000066400000000000000000000103311313450123100177530ustar00rootroot00000000000000package distribution import ( "fmt" "mime" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" ) // Manifest represents a registry object specifying a set of // references and an optional target type Manifest interface { // References returns a list of objects which make up this manifest. // A reference is anything which can be represented by a // distribution.Descriptor. These can consist of layers, resources or other // manifests. // // While no particular order is required, implementations should return // them from highest to lowest priority. For example, one might want to // return the base layer before the top layer. References() []Descriptor // Payload provides the serialized format of the manifest, in addition to // the mediatype. Payload() (mediatype string, payload []byte, err error) } // ManifestBuilder creates a manifest allowing one to include dependencies. // Instances can be obtained from a version-specific manifest package. Manifest // specific data is passed into the function which creates the builder. type ManifestBuilder interface { // Build creates the manifest from his builder. Build(ctx context.Context) (Manifest, error) // References returns a list of objects which have been added to this // builder. The dependencies are returned in the order they were added, // which should be from base to head. References() []Descriptor // AppendReference includes the given object in the manifest after any // existing dependencies. If the add fails, such as when adding an // unsupported dependency, an error may be returned. // // The destination of the reference is dependent on the manifest type and // the dependency type. AppendReference(dependency Describable) error } // ManifestService describes operations on image manifests. type ManifestService interface { // Exists returns true if the manifest exists. Exists(ctx context.Context, dgst digest.Digest) (bool, error) // Get retrieves the manifest specified by the given digest Get(ctx context.Context, dgst digest.Digest, options ...ManifestServiceOption) (Manifest, error) // Put creates or updates the given manifest returning the manifest digest Put(ctx context.Context, manifest Manifest, options ...ManifestServiceOption) (digest.Digest, error) // Delete removes the manifest specified by the given digest. Deleting // a manifest that doesn't exist will return ErrManifestNotFound Delete(ctx context.Context, dgst digest.Digest) error } // ManifestEnumerator enables iterating over manifests type ManifestEnumerator interface { // Enumerate calls ingester for each manifest. Enumerate(ctx context.Context, ingester func(digest.Digest) error) error } // Describable is an interface for descriptors type Describable interface { Descriptor() Descriptor } // ManifestMediaTypes returns the supported media types for manifests. func ManifestMediaTypes() (mediaTypes []string) { for t := range mappings { if t != "" { mediaTypes = append(mediaTypes, t) } } return } // UnmarshalFunc implements manifest unmarshalling a given MediaType type UnmarshalFunc func([]byte) (Manifest, Descriptor, error) var mappings = make(map[string]UnmarshalFunc, 0) // UnmarshalManifest looks up manifest unmarshal functions based on // MediaType func UnmarshalManifest(ctHeader string, p []byte) (Manifest, Descriptor, error) { // Need to look up by the actual media type, not the raw contents of // the header. Strip semicolons and anything following them. var mediatype string if ctHeader != "" { var err error mediatype, _, err = mime.ParseMediaType(ctHeader) if err != nil { return nil, Descriptor{}, err } } unmarshalFunc, ok := mappings[mediatype] if !ok { unmarshalFunc, ok = mappings[""] if !ok { return nil, Descriptor{}, fmt.Errorf("unsupported manifest mediatype and no default available: %s", mediatype) } } return unmarshalFunc(p) } // RegisterManifestSchema registers an UnmarshalFunc for a given schema type. This // should be called from specific func RegisterManifestSchema(mediatype string, u UnmarshalFunc) error { if _, ok := mappings[mediatype]; ok { return fmt.Errorf("manifest mediatype registration would overwrite existing: %s", mediatype) } mappings[mediatype] = u return nil } docker-registry-2.6.2~ds1/notifications/000077500000000000000000000000001313450123100203065ustar00rootroot00000000000000docker-registry-2.6.2~ds1/notifications/bridge.go000066400000000000000000000134031313450123100220720ustar00rootroot00000000000000package notifications import ( "net/http" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/uuid" ) type bridge struct { ub URLBuilder actor ActorRecord source SourceRecord request RequestRecord sink Sink } var _ Listener = &bridge{} // URLBuilder defines a subset of url builder to be used by the event listener. type URLBuilder interface { BuildManifestURL(name reference.Named) (string, error) BuildBlobURL(ref reference.Canonical) (string, error) } // NewBridge returns a notification listener that writes records to sink, // using the actor and source. Any urls populated in the events created by // this bridge will be created using the URLBuilder. // TODO(stevvooe): Update this to simply take a context.Context object. func NewBridge(ub URLBuilder, source SourceRecord, actor ActorRecord, request RequestRecord, sink Sink) Listener { return &bridge{ ub: ub, actor: actor, source: source, request: request, sink: sink, } } // NewRequestRecord builds a RequestRecord for use in NewBridge from an // http.Request, associating it with a request id. func NewRequestRecord(id string, r *http.Request) RequestRecord { return RequestRecord{ ID: id, Addr: context.RemoteAddr(r), Host: r.Host, Method: r.Method, UserAgent: r.UserAgent(), } } func (b *bridge) ManifestPushed(repo reference.Named, sm distribution.Manifest, options ...distribution.ManifestServiceOption) error { manifestEvent, err := b.createManifestEvent(EventActionPush, repo, sm) if err != nil { return err } for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { manifestEvent.Target.Tag = opt.Tag break } } return b.sink.Write(*manifestEvent) } func (b *bridge) ManifestPulled(repo reference.Named, sm distribution.Manifest, options ...distribution.ManifestServiceOption) error { manifestEvent, err := b.createManifestEvent(EventActionPull, repo, sm) if err != nil { return err } for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { manifestEvent.Target.Tag = opt.Tag break } } return b.sink.Write(*manifestEvent) } func (b *bridge) ManifestDeleted(repo reference.Named, dgst digest.Digest) error { return b.createManifestDeleteEventAndWrite(EventActionDelete, repo, dgst) } func (b *bridge) BlobPushed(repo reference.Named, desc distribution.Descriptor) error { return b.createBlobEventAndWrite(EventActionPush, repo, desc) } func (b *bridge) BlobPulled(repo reference.Named, desc distribution.Descriptor) error { return b.createBlobEventAndWrite(EventActionPull, repo, desc) } func (b *bridge) BlobMounted(repo reference.Named, desc distribution.Descriptor, fromRepo reference.Named) error { event, err := b.createBlobEvent(EventActionMount, repo, desc) if err != nil { return err } event.Target.FromRepository = fromRepo.Name() return b.sink.Write(*event) } func (b *bridge) BlobDeleted(repo reference.Named, dgst digest.Digest) error { return b.createBlobDeleteEventAndWrite(EventActionDelete, repo, dgst) } func (b *bridge) createManifestEventAndWrite(action string, repo reference.Named, sm distribution.Manifest) error { manifestEvent, err := b.createManifestEvent(action, repo, sm) if err != nil { return err } return b.sink.Write(*manifestEvent) } func (b *bridge) createManifestDeleteEventAndWrite(action string, repo reference.Named, dgst digest.Digest) error { event := b.createEvent(action) event.Target.Repository = repo.Name() event.Target.Digest = dgst return b.sink.Write(*event) } func (b *bridge) createManifestEvent(action string, repo reference.Named, sm distribution.Manifest) (*Event, error) { event := b.createEvent(action) event.Target.Repository = repo.Name() mt, p, err := sm.Payload() if err != nil { return nil, err } // Ensure we have the canonical manifest descriptor here _, desc, err := distribution.UnmarshalManifest(mt, p) if err != nil { return nil, err } event.Target.MediaType = mt event.Target.Length = desc.Size event.Target.Size = desc.Size event.Target.Digest = desc.Digest ref, err := reference.WithDigest(repo, event.Target.Digest) if err != nil { return nil, err } event.Target.URL, err = b.ub.BuildManifestURL(ref) if err != nil { return nil, err } return event, nil } func (b *bridge) createBlobDeleteEventAndWrite(action string, repo reference.Named, dgst digest.Digest) error { event := b.createEvent(action) event.Target.Digest = dgst event.Target.Repository = repo.Name() return b.sink.Write(*event) } func (b *bridge) createBlobEventAndWrite(action string, repo reference.Named, desc distribution.Descriptor) error { event, err := b.createBlobEvent(action, repo, desc) if err != nil { return err } return b.sink.Write(*event) } func (b *bridge) createBlobEvent(action string, repo reference.Named, desc distribution.Descriptor) (*Event, error) { event := b.createEvent(action) event.Target.Descriptor = desc event.Target.Length = desc.Size event.Target.Repository = repo.Name() ref, err := reference.WithDigest(repo, desc.Digest) if err != nil { return nil, err } event.Target.URL, err = b.ub.BuildBlobURL(ref) if err != nil { return nil, err } return event, nil } // createEvent creates an event with actor and source populated. func (b *bridge) createEvent(action string) *Event { event := createEvent(action) event.Source = b.source event.Actor = b.actor event.Request = b.request return event } // createEvent returns a new event, timestamped, with the specified action. func createEvent(action string) *Event { return &Event{ ID: uuid.Generate().String(), Timestamp: time.Now(), Action: action, } } docker-registry-2.6.2~ds1/notifications/bridge_test.go000066400000000000000000000127611313450123100231370ustar00rootroot00000000000000package notifications import ( "testing" "github.com/docker/distribution" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/uuid" "github.com/docker/libtrust" ) var ( // common environment for expected manifest events. repo = "test/repo" source = SourceRecord{ Addr: "remote.test", InstanceID: uuid.Generate().String(), } ub = mustUB(v2.NewURLBuilderFromString("http://test.example.com/", false)) actor = ActorRecord{ Name: "test", } request = RequestRecord{} m = schema1.Manifest{ Name: repo, Tag: "latest", } sm *schema1.SignedManifest payload []byte dgst digest.Digest ) func TestEventBridgeManifestPulled(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkCommonManifest(t, EventActionPull, events...) return nil })) repoRef, _ := reference.ParseNamed(repo) if err := l.ManifestPulled(repoRef, sm); err != nil { t.Fatalf("unexpected error notifying manifest pull: %v", err) } } func TestEventBridgeManifestPushed(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkCommonManifest(t, EventActionPush, events...) return nil })) repoRef, _ := reference.ParseNamed(repo) if err := l.ManifestPushed(repoRef, sm); err != nil { t.Fatalf("unexpected error notifying manifest pull: %v", err) } } func TestEventBridgeManifestPushedWithTag(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkCommonManifest(t, EventActionPush, events...) if events[0].Target.Tag != "latest" { t.Fatalf("missing or unexpected tag: %#v", events[0].Target) } return nil })) repoRef, _ := reference.ParseNamed(repo) if err := l.ManifestPushed(repoRef, sm, distribution.WithTag(m.Tag)); err != nil { t.Fatalf("unexpected error notifying manifest pull: %v", err) } } func TestEventBridgeManifestPulledWithTag(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkCommonManifest(t, EventActionPull, events...) if events[0].Target.Tag != "latest" { t.Fatalf("missing or unexpected tag: %#v", events[0].Target) } return nil })) repoRef, _ := reference.ParseNamed(repo) if err := l.ManifestPulled(repoRef, sm, distribution.WithTag(m.Tag)); err != nil { t.Fatalf("unexpected error notifying manifest pull: %v", err) } } func TestEventBridgeManifestDeleted(t *testing.T) { l := createTestEnv(t, testSinkFn(func(events ...Event) error { checkDeleted(t, EventActionDelete, events...) return nil })) repoRef, _ := reference.ParseNamed(repo) if err := l.ManifestDeleted(repoRef, dgst); err != nil { t.Fatalf("unexpected error notifying manifest pull: %v", err) } } func createTestEnv(t *testing.T, fn testSinkFn) Listener { pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("error generating private key: %v", err) } sm, err = schema1.Sign(&m, pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } payload = sm.Canonical dgst = digest.FromBytes(payload) return NewBridge(ub, source, actor, request, fn) } func checkDeleted(t *testing.T, action string, events ...Event) { if len(events) != 1 { t.Fatalf("unexpected number of events: %v != 1", len(events)) } event := events[0] if event.Source != source { t.Fatalf("source not equal: %#v != %#v", event.Source, source) } if event.Request != request { t.Fatalf("request not equal: %#v != %#v", event.Request, request) } if event.Actor != actor { t.Fatalf("request not equal: %#v != %#v", event.Actor, actor) } if event.Target.Digest != dgst { t.Fatalf("unexpected digest on event target: %q != %q", event.Target.Digest, dgst) } if event.Target.Repository != repo { t.Fatalf("unexpected repository: %q != %q", event.Target.Repository, repo) } } func checkCommonManifest(t *testing.T, action string, events ...Event) { checkCommon(t, events...) event := events[0] if event.Action != action { t.Fatalf("unexpected event action: %q != %q", event.Action, action) } repoRef, _ := reference.ParseNamed(repo) ref, _ := reference.WithDigest(repoRef, dgst) u, err := ub.BuildManifestURL(ref) if err != nil { t.Fatalf("error building expected url: %v", err) } if event.Target.URL != u { t.Fatalf("incorrect url passed: \n%q != \n%q", event.Target.URL, u) } } func checkCommon(t *testing.T, events ...Event) { if len(events) != 1 { t.Fatalf("unexpected number of events: %v != 1", len(events)) } event := events[0] if event.Source != source { t.Fatalf("source not equal: %#v != %#v", event.Source, source) } if event.Request != request { t.Fatalf("request not equal: %#v != %#v", event.Request, request) } if event.Actor != actor { t.Fatalf("request not equal: %#v != %#v", event.Actor, actor) } if event.Target.Digest != dgst { t.Fatalf("unexpected digest on event target: %q != %q", event.Target.Digest, dgst) } if event.Target.Length != int64(len(payload)) { t.Fatalf("unexpected target length: %v != %v", event.Target.Length, len(payload)) } if event.Target.Repository != repo { t.Fatalf("unexpected repository: %q != %q", event.Target.Repository, repo) } } type testSinkFn func(events ...Event) error func (tsf testSinkFn) Write(events ...Event) error { return tsf(events...) } func (tsf testSinkFn) Close() error { return nil } func mustUB(ub *v2.URLBuilder, err error) *v2.URLBuilder { if err != nil { panic(err) } return ub } docker-registry-2.6.2~ds1/notifications/endpoint.go000066400000000000000000000045021313450123100224560ustar00rootroot00000000000000package notifications import ( "net/http" "time" ) // EndpointConfig covers the optional configuration parameters for an active // endpoint. type EndpointConfig struct { Headers http.Header Timeout time.Duration Threshold int Backoff time.Duration IgnoredMediaTypes []string Transport *http.Transport } // defaults set any zero-valued fields to a reasonable default. func (ec *EndpointConfig) defaults() { if ec.Timeout <= 0 { ec.Timeout = time.Second } if ec.Threshold <= 0 { ec.Threshold = 10 } if ec.Backoff <= 0 { ec.Backoff = time.Second } if ec.Transport == nil { ec.Transport = http.DefaultTransport.(*http.Transport) } } // Endpoint is a reliable, queued, thread-safe sink that notify external http // services when events are written. Writes are non-blocking and always // succeed for callers but events may be queued internally. type Endpoint struct { Sink url string name string EndpointConfig metrics *safeMetrics } // NewEndpoint returns a running endpoint, ready to receive events. func NewEndpoint(name, url string, config EndpointConfig) *Endpoint { var endpoint Endpoint endpoint.name = name endpoint.url = url endpoint.EndpointConfig = config endpoint.defaults() endpoint.metrics = newSafeMetrics() // Configures the inmemory queue, retry, http pipeline. endpoint.Sink = newHTTPSink( endpoint.url, endpoint.Timeout, endpoint.Headers, endpoint.Transport, endpoint.metrics.httpStatusListener()) endpoint.Sink = newRetryingSink(endpoint.Sink, endpoint.Threshold, endpoint.Backoff) endpoint.Sink = newEventQueue(endpoint.Sink, endpoint.metrics.eventQueueListener()) endpoint.Sink = newIgnoredMediaTypesSink(endpoint.Sink, config.IgnoredMediaTypes) register(&endpoint) return &endpoint } // Name returns the name of the endpoint, generally used for debugging. func (e *Endpoint) Name() string { return e.name } // URL returns the url of the endpoint. func (e *Endpoint) URL() string { return e.url } // ReadMetrics populates em with metrics from the endpoint. func (e *Endpoint) ReadMetrics(em *EndpointMetrics) { e.metrics.Lock() defer e.metrics.Unlock() *em = e.metrics.EndpointMetrics // Map still need to copied in a threadsafe manner. em.Statuses = make(map[string]int) for k, v := range e.metrics.Statuses { em.Statuses[k] = v } } docker-registry-2.6.2~ds1/notifications/event.go000066400000000000000000000130371313450123100217620ustar00rootroot00000000000000package notifications import ( "fmt" "time" "github.com/docker/distribution" ) // EventAction constants used in action field of Event. const ( EventActionPull = "pull" EventActionPush = "push" EventActionMount = "mount" EventActionDelete = "delete" ) const ( // EventsMediaType is the mediatype for the json event envelope. If the // Event, ActorRecord, SourceRecord or Envelope structs change, the version // number should be incremented. EventsMediaType = "application/vnd.docker.distribution.events.v1+json" // LayerMediaType is the media type for image rootfs diffs (aka "layers") // used by Docker. We don't expect this to change for quite a while. layerMediaType = "application/vnd.docker.container.image.rootfs.diff+x-gtar" ) // Envelope defines the fields of a json event envelope message that can hold // one or more events. type Envelope struct { // Events make up the contents of the envelope. Events present in a single // envelope are not necessarily related. Events []Event `json:"events,omitempty"` } // TODO(stevvooe): The event type should be separate from the json format. It // should be defined as an interface. Leaving as is for now since we don't // need that at this time. If we make this change, the struct below would be // called "EventRecord". // Event provides the fields required to describe a registry event. type Event struct { // ID provides a unique identifier for the event. ID string `json:"id,omitempty"` // Timestamp is the time at which the event occurred. Timestamp time.Time `json:"timestamp,omitempty"` // Action indicates what action encompasses the provided event. Action string `json:"action,omitempty"` // Target uniquely describes the target of the event. Target struct { // TODO(stevvooe): Use http.DetectContentType for layers, maybe. distribution.Descriptor // Length in bytes of content. Same as Size field in Descriptor. // Provided for backwards compatibility. Length int64 `json:"length,omitempty"` // Repository identifies the named repository. Repository string `json:"repository,omitempty"` // FromRepository identifies the named repository which a blob was mounted // from if appropriate. FromRepository string `json:"fromRepository,omitempty"` // URL provides a direct link to the content. URL string `json:"url,omitempty"` // Tag provides the tag Tag string `json:"tag,omitempty"` } `json:"target,omitempty"` // Request covers the request that generated the event. Request RequestRecord `json:"request,omitempty"` // Actor specifies the agent that initiated the event. For most // situations, this could be from the authorizaton context of the request. Actor ActorRecord `json:"actor,omitempty"` // Source identifies the registry node that generated the event. Put // differently, while the actor "initiates" the event, the source // "generates" it. Source SourceRecord `json:"source,omitempty"` } // ActorRecord specifies the agent that initiated the event. For most // situations, this could be from the authorizaton context of the request. // Data in this record can refer to both the initiating client and the // generating request. type ActorRecord struct { // Name corresponds to the subject or username associated with the // request context that generated the event. Name string `json:"name,omitempty"` // TODO(stevvooe): Look into setting a session cookie to get this // without docker daemon. // SessionID // TODO(stevvooe): Push the "Docker-Command" header to replace cookie and // get the actual command. // Command } // RequestRecord covers the request that generated the event. type RequestRecord struct { // ID uniquely identifies the request that initiated the event. ID string `json:"id"` // Addr contains the ip or hostname and possibly port of the client // connection that initiated the event. This is the RemoteAddr from // the standard http request. Addr string `json:"addr,omitempty"` // Host is the externally accessible host name of the registry instance, // as specified by the http host header on incoming requests. Host string `json:"host,omitempty"` // Method has the request method that generated the event. Method string `json:"method"` // UserAgent contains the user agent header of the request. UserAgent string `json:"useragent"` } // SourceRecord identifies the registry node that generated the event. Put // differently, while the actor "initiates" the event, the source "generates" // it. type SourceRecord struct { // Addr contains the ip or hostname and the port of the registry node // that generated the event. Generally, this will be resolved by // os.Hostname() along with the running port. Addr string `json:"addr,omitempty"` // InstanceID identifies a running instance of an application. Changes // after each restart. InstanceID string `json:"instanceID,omitempty"` } var ( // ErrSinkClosed is returned if a write is issued to a sink that has been // closed. If encountered, the error should be considered terminal and // retries will not be successful. ErrSinkClosed = fmt.Errorf("sink: closed") ) // Sink accepts and sends events. type Sink interface { // Write writes one or more events to the sink. If no error is returned, // the caller will assume that all events have been committed and will not // try to send them again. If an error is received, the caller may retry // sending the event. The caller should cede the slice of memory to the // sink and not modify it after calling this method. Write(events ...Event) error // Close the sink, possibly waiting for pending events to flush. Close() error } docker-registry-2.6.2~ds1/notifications/event_test.go000066400000000000000000000116671313450123100230300ustar00rootroot00000000000000package notifications import ( "encoding/json" "strings" "testing" "time" "github.com/docker/distribution/manifest/schema1" ) // TestEventJSONFormat provides silly test to detect if the event format or // envelope has changed. If this code fails, the revision of the protocol may // need to be incremented. func TestEventEnvelopeJSONFormat(t *testing.T) { var expected = strings.TrimSpace(` { "events": [ { "id": "asdf-asdf-asdf-asdf-0", "timestamp": "2006-01-02T15:04:05Z", "action": "push", "target": { "mediaType": "application/vnd.docker.distribution.manifest.v1+prettyjws", "size": 1, "digest": "sha256:0123456789abcdef0", "length": 1, "repository": "library/test", "url": "http://example.com/v2/library/test/manifests/latest" }, "request": { "id": "asdfasdf", "addr": "client.local", "host": "registrycluster.local", "method": "PUT", "useragent": "test/0.1" }, "actor": { "name": "test-actor" }, "source": { "addr": "hostname.local:port" } }, { "id": "asdf-asdf-asdf-asdf-1", "timestamp": "2006-01-02T15:04:05Z", "action": "push", "target": { "mediaType": "application/vnd.docker.container.image.rootfs.diff+x-gtar", "size": 2, "digest": "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5", "length": 2, "repository": "library/test", "url": "http://example.com/v2/library/test/manifests/latest" }, "request": { "id": "asdfasdf", "addr": "client.local", "host": "registrycluster.local", "method": "PUT", "useragent": "test/0.1" }, "actor": { "name": "test-actor" }, "source": { "addr": "hostname.local:port" } }, { "id": "asdf-asdf-asdf-asdf-2", "timestamp": "2006-01-02T15:04:05Z", "action": "push", "target": { "mediaType": "application/vnd.docker.container.image.rootfs.diff+x-gtar", "size": 3, "digest": "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d6", "length": 3, "repository": "library/test", "url": "http://example.com/v2/library/test/manifests/latest" }, "request": { "id": "asdfasdf", "addr": "client.local", "host": "registrycluster.local", "method": "PUT", "useragent": "test/0.1" }, "actor": { "name": "test-actor" }, "source": { "addr": "hostname.local:port" } } ] } `) tm, err := time.Parse(time.RFC3339, time.RFC3339[:len(time.RFC3339)-5]) if err != nil { t.Fatalf("error creating time: %v", err) } var prototype Event prototype.Action = EventActionPush prototype.Timestamp = tm prototype.Actor.Name = "test-actor" prototype.Request.ID = "asdfasdf" prototype.Request.Addr = "client.local" prototype.Request.Host = "registrycluster.local" prototype.Request.Method = "PUT" prototype.Request.UserAgent = "test/0.1" prototype.Source.Addr = "hostname.local:port" var manifestPush Event manifestPush = prototype manifestPush.ID = "asdf-asdf-asdf-asdf-0" manifestPush.Target.Digest = "sha256:0123456789abcdef0" manifestPush.Target.Length = 1 manifestPush.Target.Size = 1 manifestPush.Target.MediaType = schema1.MediaTypeSignedManifest manifestPush.Target.Repository = "library/test" manifestPush.Target.URL = "http://example.com/v2/library/test/manifests/latest" var layerPush0 Event layerPush0 = prototype layerPush0.ID = "asdf-asdf-asdf-asdf-1" layerPush0.Target.Digest = "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5" layerPush0.Target.Length = 2 layerPush0.Target.Size = 2 layerPush0.Target.MediaType = layerMediaType layerPush0.Target.Repository = "library/test" layerPush0.Target.URL = "http://example.com/v2/library/test/manifests/latest" var layerPush1 Event layerPush1 = prototype layerPush1.ID = "asdf-asdf-asdf-asdf-2" layerPush1.Target.Digest = "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d6" layerPush1.Target.Length = 3 layerPush1.Target.Size = 3 layerPush1.Target.MediaType = layerMediaType layerPush1.Target.Repository = "library/test" layerPush1.Target.URL = "http://example.com/v2/library/test/manifests/latest" var envelope Envelope envelope.Events = append(envelope.Events, manifestPush, layerPush0, layerPush1) p, err := json.MarshalIndent(envelope, "", " ") if err != nil { t.Fatalf("unexpected error marshaling envelope: %v", err) } if string(p) != expected { t.Fatalf("format has changed\n%s\n != \n%s", string(p), expected) } } docker-registry-2.6.2~ds1/notifications/http.go000066400000000000000000000074531313450123100216250ustar00rootroot00000000000000package notifications import ( "bytes" "encoding/json" "fmt" "net/http" "sync" "time" ) // httpSink implements a single-flight, http notification endpoint. This is // very lightweight in that it only makes an attempt at an http request. // Reliability should be provided by the caller. type httpSink struct { url string mu sync.Mutex closed bool client *http.Client listeners []httpStatusListener // TODO(stevvooe): Allow one to configure the media type accepted by this // sink and choose the serialization based on that. } // newHTTPSink returns an unreliable, single-flight http sink. Wrap in other // sinks for increased reliability. func newHTTPSink(u string, timeout time.Duration, headers http.Header, transport *http.Transport, listeners ...httpStatusListener) *httpSink { if transport == nil { transport = http.DefaultTransport.(*http.Transport) } return &httpSink{ url: u, listeners: listeners, client: &http.Client{ Transport: &headerRoundTripper{ Transport: transport, headers: headers, }, Timeout: timeout, }, } } // httpStatusListener is called on various outcomes of sending notifications. type httpStatusListener interface { success(status int, events ...Event) failure(status int, events ...Event) err(err error, events ...Event) } // Accept makes an attempt to notify the endpoint, returning an error if it // fails. It is the caller's responsibility to retry on error. The events are // accepted or rejected as a group. func (hs *httpSink) Write(events ...Event) error { hs.mu.Lock() defer hs.mu.Unlock() defer hs.client.Transport.(*headerRoundTripper).CloseIdleConnections() if hs.closed { return ErrSinkClosed } envelope := Envelope{ Events: events, } // TODO(stevvooe): It is not ideal to keep re-encoding the request body on // retry but we are going to do it to keep the code simple. It is likely // we could change the event struct to manage its own buffer. p, err := json.MarshalIndent(envelope, "", " ") if err != nil { for _, listener := range hs.listeners { listener.err(err, events...) } return fmt.Errorf("%v: error marshaling event envelope: %v", hs, err) } body := bytes.NewReader(p) resp, err := hs.client.Post(hs.url, EventsMediaType, body) if err != nil { for _, listener := range hs.listeners { listener.err(err, events...) } return fmt.Errorf("%v: error posting: %v", hs, err) } defer resp.Body.Close() // The notifier will treat any 2xx or 3xx response as accepted by the // endpoint. switch { case resp.StatusCode >= 200 && resp.StatusCode < 400: for _, listener := range hs.listeners { listener.success(resp.StatusCode, events...) } // TODO(stevvooe): This is a little accepting: we may want to support // unsupported media type responses with retries using the correct // media type. There may also be cases that will never work. return nil default: for _, listener := range hs.listeners { listener.failure(resp.StatusCode, events...) } return fmt.Errorf("%v: response status %v unaccepted", hs, resp.Status) } } // Close the endpoint func (hs *httpSink) Close() error { hs.mu.Lock() defer hs.mu.Unlock() if hs.closed { return fmt.Errorf("httpsink: already closed") } hs.closed = true return nil } func (hs *httpSink) String() string { return fmt.Sprintf("httpSink{%s}", hs.url) } type headerRoundTripper struct { *http.Transport // must be transport to support CancelRequest headers http.Header } func (hrt *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { var nreq http.Request nreq = *req nreq.Header = make(http.Header) merge := func(headers http.Header) { for k, v := range headers { nreq.Header[k] = append(nreq.Header[k], v...) } } merge(req.Header) merge(hrt.headers) return hrt.Transport.RoundTrip(&nreq) } docker-registry-2.6.2~ds1/notifications/http_test.go000066400000000000000000000115651313450123100226630ustar00rootroot00000000000000package notifications import ( "crypto/tls" "encoding/json" "fmt" "mime" "net/http" "net/http/httptest" "reflect" "strconv" "strings" "testing" "github.com/docker/distribution/manifest/schema1" ) // TestHTTPSink mocks out an http endpoint and notifies it under a couple of // conditions, ensuring correct behavior. func TestHTTPSink(t *testing.T) { serverHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) t.Fatalf("unexpected request method: %v", r.Method) return } // Extract the content type and make sure it matches contentType := r.Header.Get("Content-Type") mediaType, _, err := mime.ParseMediaType(contentType) if err != nil { w.WriteHeader(http.StatusBadRequest) t.Fatalf("error parsing media type: %v, contenttype=%q", err, contentType) return } if mediaType != EventsMediaType { w.WriteHeader(http.StatusUnsupportedMediaType) t.Fatalf("incorrect media type: %q != %q", mediaType, EventsMediaType) return } var envelope Envelope dec := json.NewDecoder(r.Body) if err := dec.Decode(&envelope); err != nil { w.WriteHeader(http.StatusBadRequest) t.Fatalf("error decoding request body: %v", err) return } // Let caller choose the status status, err := strconv.Atoi(r.FormValue("status")) if err != nil { t.Logf("error parsing status: %v", err) // May just be empty, set status to 200 status = http.StatusOK } w.WriteHeader(status) }) server := httptest.NewTLSServer(serverHandler) metrics := newSafeMetrics() sink := newHTTPSink(server.URL, 0, nil, nil, &endpointMetricsHTTPStatusListener{safeMetrics: metrics}) // first make sure that the default transport gives x509 untrusted cert error events := []Event{} err := sink.Write(events...) if !strings.Contains(err.Error(), "x509") { t.Fatal("TLS server with default transport should give unknown CA error") } if err := sink.Close(); err != nil { t.Fatalf("unexpected error closing http sink: %v", err) } // make sure that passing in the transport no longer gives this error tr := &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } sink = newHTTPSink(server.URL, 0, nil, tr, &endpointMetricsHTTPStatusListener{safeMetrics: metrics}) err = sink.Write(events...) if err != nil { t.Fatalf("unexpected error writing events: %v", err) } // reset server to standard http server and sink to a basic sink server = httptest.NewServer(serverHandler) sink = newHTTPSink(server.URL, 0, nil, nil, &endpointMetricsHTTPStatusListener{safeMetrics: metrics}) var expectedMetrics EndpointMetrics expectedMetrics.Statuses = make(map[string]int) for _, tc := range []struct { events []Event // events to send url string failure bool // true if there should be a failure. statusCode int // if not set, no status code should be incremented. }{ { statusCode: http.StatusOK, events: []Event{ createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest)}, }, { statusCode: http.StatusOK, events: []Event{ createTestEvent("push", "library/test", schema1.MediaTypeSignedManifest), createTestEvent("push", "library/test", layerMediaType), createTestEvent("push", "library/test", layerMediaType), }, }, { statusCode: http.StatusTemporaryRedirect, }, { statusCode: http.StatusBadRequest, failure: true, }, { // Case where connection never goes through. url: "http://shoudlntresolve/", failure: true, }, } { if tc.failure { expectedMetrics.Failures += len(tc.events) } else { expectedMetrics.Successes += len(tc.events) } if tc.statusCode > 0 { expectedMetrics.Statuses[fmt.Sprintf("%d %s", tc.statusCode, http.StatusText(tc.statusCode))] += len(tc.events) } url := tc.url if url == "" { url = server.URL + "/" } // setup endpoint to respond with expected status code. url += fmt.Sprintf("?status=%v", tc.statusCode) sink.url = url t.Logf("testcase: %v, fail=%v", url, tc.failure) // Try a simple event emission. err := sink.Write(tc.events...) if !tc.failure { if err != nil { t.Fatalf("unexpected error send event: %v", err) } } else { if err == nil { t.Fatalf("the endpoint should have rejected the request") } } if !reflect.DeepEqual(metrics.EndpointMetrics, expectedMetrics) { t.Fatalf("metrics not as expected: %#v != %#v", metrics.EndpointMetrics, expectedMetrics) } } if err := sink.Close(); err != nil { t.Fatalf("unexpected error closing http sink: %v", err) } // double close returns error if err := sink.Close(); err == nil { t.Fatalf("second close should have returned error: %v", err) } } func createTestEvent(action, repo, typ string) Event { event := createEvent(action) event.Target.MediaType = typ event.Target.Repository = repo return *event } docker-registry-2.6.2~ds1/notifications/listener.go000066400000000000000000000162141313450123100224660ustar00rootroot00000000000000package notifications import ( "net/http" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" ) // ManifestListener describes a set of methods for listening to events related to manifests. type ManifestListener interface { ManifestPushed(repo reference.Named, sm distribution.Manifest, options ...distribution.ManifestServiceOption) error ManifestPulled(repo reference.Named, sm distribution.Manifest, options ...distribution.ManifestServiceOption) error ManifestDeleted(repo reference.Named, dgst digest.Digest) error } // BlobListener describes a listener that can respond to layer related events. type BlobListener interface { BlobPushed(repo reference.Named, desc distribution.Descriptor) error BlobPulled(repo reference.Named, desc distribution.Descriptor) error BlobMounted(repo reference.Named, desc distribution.Descriptor, fromRepo reference.Named) error BlobDeleted(repo reference.Named, desc digest.Digest) error } // Listener combines all repository events into a single interface. type Listener interface { ManifestListener BlobListener } type repositoryListener struct { distribution.Repository listener Listener } // Listen dispatches events on the repository to the listener. func Listen(repo distribution.Repository, listener Listener) distribution.Repository { return &repositoryListener{ Repository: repo, listener: listener, } } func (rl *repositoryListener) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { manifests, err := rl.Repository.Manifests(ctx, options...) if err != nil { return nil, err } return &manifestServiceListener{ ManifestService: manifests, parent: rl, }, nil } func (rl *repositoryListener) Blobs(ctx context.Context) distribution.BlobStore { return &blobServiceListener{ BlobStore: rl.Repository.Blobs(ctx), parent: rl, } } type manifestServiceListener struct { distribution.ManifestService parent *repositoryListener } func (msl *manifestServiceListener) Delete(ctx context.Context, dgst digest.Digest) error { err := msl.ManifestService.Delete(ctx, dgst) if err == nil { if err := msl.parent.listener.ManifestDeleted(msl.parent.Repository.Named(), dgst); err != nil { context.GetLogger(ctx).Errorf("error dispatching manifest delete to listener: %v", err) } } return err } func (msl *manifestServiceListener) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { sm, err := msl.ManifestService.Get(ctx, dgst, options...) if err == nil { if err := msl.parent.listener.ManifestPulled(msl.parent.Repository.Named(), sm, options...); err != nil { context.GetLogger(ctx).Errorf("error dispatching manifest pull to listener: %v", err) } } return sm, err } func (msl *manifestServiceListener) Put(ctx context.Context, sm distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { dgst, err := msl.ManifestService.Put(ctx, sm, options...) if err == nil { if err := msl.parent.listener.ManifestPushed(msl.parent.Repository.Named(), sm, options...); err != nil { context.GetLogger(ctx).Errorf("error dispatching manifest push to listener: %v", err) } } return dgst, err } type blobServiceListener struct { distribution.BlobStore parent *repositoryListener } var _ distribution.BlobStore = &blobServiceListener{} func (bsl *blobServiceListener) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { p, err := bsl.BlobStore.Get(ctx, dgst) if err == nil { if desc, err := bsl.Stat(ctx, dgst); err != nil { context.GetLogger(ctx).Errorf("error resolving descriptor in ServeBlob listener: %v", err) } else { if err := bsl.parent.listener.BlobPulled(bsl.parent.Repository.Named(), desc); err != nil { context.GetLogger(ctx).Errorf("error dispatching layer pull to listener: %v", err) } } } return p, err } func (bsl *blobServiceListener) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { rc, err := bsl.BlobStore.Open(ctx, dgst) if err == nil { if desc, err := bsl.Stat(ctx, dgst); err != nil { context.GetLogger(ctx).Errorf("error resolving descriptor in ServeBlob listener: %v", err) } else { if err := bsl.parent.listener.BlobPulled(bsl.parent.Repository.Named(), desc); err != nil { context.GetLogger(ctx).Errorf("error dispatching layer pull to listener: %v", err) } } } return rc, err } func (bsl *blobServiceListener) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { err := bsl.BlobStore.ServeBlob(ctx, w, r, dgst) if err == nil { if desc, err := bsl.Stat(ctx, dgst); err != nil { context.GetLogger(ctx).Errorf("error resolving descriptor in ServeBlob listener: %v", err) } else { if err := bsl.parent.listener.BlobPulled(bsl.parent.Repository.Named(), desc); err != nil { context.GetLogger(ctx).Errorf("error dispatching layer pull to listener: %v", err) } } } return err } func (bsl *blobServiceListener) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { desc, err := bsl.BlobStore.Put(ctx, mediaType, p) if err == nil { if err := bsl.parent.listener.BlobPushed(bsl.parent.Repository.Named(), desc); err != nil { context.GetLogger(ctx).Errorf("error dispatching layer push to listener: %v", err) } } return desc, err } func (bsl *blobServiceListener) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { wr, err := bsl.BlobStore.Create(ctx, options...) switch err := err.(type) { case distribution.ErrBlobMounted: if err := bsl.parent.listener.BlobMounted(bsl.parent.Repository.Named(), err.Descriptor, err.From); err != nil { context.GetLogger(ctx).Errorf("error dispatching blob mount to listener: %v", err) } return nil, err } return bsl.decorateWriter(wr), err } func (bsl *blobServiceListener) Delete(ctx context.Context, dgst digest.Digest) error { err := bsl.BlobStore.Delete(ctx, dgst) if err == nil { if err := bsl.parent.listener.BlobDeleted(bsl.parent.Repository.Named(), dgst); err != nil { context.GetLogger(ctx).Errorf("error dispatching layer delete to listener: %v", err) } } return err } func (bsl *blobServiceListener) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { wr, err := bsl.BlobStore.Resume(ctx, id) return bsl.decorateWriter(wr), err } func (bsl *blobServiceListener) decorateWriter(wr distribution.BlobWriter) distribution.BlobWriter { return &blobWriterListener{ BlobWriter: wr, parent: bsl, } } type blobWriterListener struct { distribution.BlobWriter parent *blobServiceListener } func (bwl *blobWriterListener) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) { committed, err := bwl.BlobWriter.Commit(ctx, desc) if err == nil { if err := bwl.parent.parent.listener.BlobPushed(bwl.parent.parent.Repository.Named(), committed); err != nil { context.GetLogger(ctx).Errorf("error dispatching blob push to listener: %v", err) } } return committed, err } docker-registry-2.6.2~ds1/notifications/listener_test.go000066400000000000000000000125211313450123100235220ustar00rootroot00000000000000package notifications import ( "io" "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" "github.com/docker/libtrust" ) func TestListener(t *testing.T) { ctx := context.Background() k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } registry, err := storage.NewRegistry(ctx, inmemory.New(), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableDelete, storage.EnableRedirect, storage.Schema1SigningKey(k)) if err != nil { t.Fatalf("error creating registry: %v", err) } tl := &testListener{ ops: make(map[string]int), } repoRef, _ := reference.ParseNamed("foo/bar") repository, err := registry.Repository(ctx, repoRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } repository = Listen(repository, tl) // Now take the registry through a number of operations checkExerciseRepository(t, repository) expectedOps := map[string]int{ "manifest:push": 1, "manifest:pull": 1, "manifest:delete": 1, "layer:push": 2, "layer:pull": 2, "layer:delete": 2, } if !reflect.DeepEqual(tl.ops, expectedOps) { t.Fatalf("counts do not match:\n%v\n !=\n%v", tl.ops, expectedOps) } } type testListener struct { ops map[string]int } func (tl *testListener) ManifestPushed(repo reference.Named, m distribution.Manifest, options ...distribution.ManifestServiceOption) error { tl.ops["manifest:push"]++ return nil } func (tl *testListener) ManifestPulled(repo reference.Named, m distribution.Manifest, options ...distribution.ManifestServiceOption) error { tl.ops["manifest:pull"]++ return nil } func (tl *testListener) ManifestDeleted(repo reference.Named, d digest.Digest) error { tl.ops["manifest:delete"]++ return nil } func (tl *testListener) BlobPushed(repo reference.Named, desc distribution.Descriptor) error { tl.ops["layer:push"]++ return nil } func (tl *testListener) BlobPulled(repo reference.Named, desc distribution.Descriptor) error { tl.ops["layer:pull"]++ return nil } func (tl *testListener) BlobMounted(repo reference.Named, desc distribution.Descriptor, fromRepo reference.Named) error { tl.ops["layer:mount"]++ return nil } func (tl *testListener) BlobDeleted(repo reference.Named, d digest.Digest) error { tl.ops["layer:delete"]++ return nil } // checkExerciseRegistry takes the registry through all of its operations, // carrying out generic checks. func checkExerciseRepository(t *testing.T, repository distribution.Repository) { // TODO(stevvooe): This would be a nice testutil function. Basically, it // takes the registry through a common set of operations. This could be // used to make cross-cutting updates by changing internals that affect // update counts. Basically, it would make writing tests a lot easier. ctx := context.Background() tag := "thetag" // todo: change this to use Builder m := schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: repository.Named().Name(), Tag: tag, } var blobDigests []digest.Digest blobs := repository.Blobs(ctx) for i := 0; i < 2; i++ { rs, ds, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating test layer: %v", err) } dgst := digest.Digest(ds) blobDigests = append(blobDigests, dgst) wr, err := blobs.Create(ctx) if err != nil { t.Fatalf("error creating layer upload: %v", err) } // Use the resumes, as well! wr, err = blobs.Resume(ctx, wr.ID()) if err != nil { t.Fatalf("error resuming layer upload: %v", err) } io.Copy(wr, rs) if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } m.FSLayers = append(m.FSLayers, schema1.FSLayer{ BlobSum: dgst, }) m.History = append(m.History, schema1.History{ V1Compatibility: "", }) // Then fetch the blobs if rc, err := blobs.Open(ctx, dgst); err != nil { t.Fatalf("error fetching layer: %v", err) } else { defer rc.Close() } } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating key: %v", err) } sm, err := schema1.Sign(&m, pk) if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } manifests, err := repository.Manifests(ctx) if err != nil { t.Fatal(err.Error()) } var digestPut digest.Digest if digestPut, err = manifests.Put(ctx, sm); err != nil { t.Fatalf("unexpected error putting the manifest: %v", err) } dgst := digest.FromBytes(sm.Canonical) if dgst != digestPut { t.Fatalf("mismatching digest from payload and put") } _, err = manifests.Get(ctx, dgst) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } err = manifests.Delete(ctx, dgst) if err != nil { t.Fatalf("unexpected error deleting blob: %v", err) } for _, d := range blobDigests { err = blobs.Delete(ctx, d) if err != nil { t.Fatalf("unexpected error deleting blob: %v", err) } } } docker-registry-2.6.2~ds1/notifications/metrics.go000066400000000000000000000100451313450123100223030ustar00rootroot00000000000000package notifications import ( "expvar" "fmt" "net/http" "sync" ) // EndpointMetrics track various actions taken by the endpoint, typically by // number of events. The goal of this to export it via expvar but we may find // some other future solution to be better. type EndpointMetrics struct { Pending int // events pending in queue Events int // total events incoming Successes int // total events written successfully Failures int // total events failed Errors int // total events errored Statuses map[string]int // status code histogram, per call event } // safeMetrics guards the metrics implementation with a lock and provides a // safe update function. type safeMetrics struct { EndpointMetrics sync.Mutex // protects statuses map } // newSafeMetrics returns safeMetrics with map allocated. func newSafeMetrics() *safeMetrics { var sm safeMetrics sm.Statuses = make(map[string]int) return &sm } // httpStatusListener returns the listener for the http sink that updates the // relevant counters. func (sm *safeMetrics) httpStatusListener() httpStatusListener { return &endpointMetricsHTTPStatusListener{ safeMetrics: sm, } } // eventQueueListener returns a listener that maintains queue related counters. func (sm *safeMetrics) eventQueueListener() eventQueueListener { return &endpointMetricsEventQueueListener{ safeMetrics: sm, } } // endpointMetricsHTTPStatusListener increments counters related to http sinks // for the relevant events. type endpointMetricsHTTPStatusListener struct { *safeMetrics } var _ httpStatusListener = &endpointMetricsHTTPStatusListener{} func (emsl *endpointMetricsHTTPStatusListener) success(status int, events ...Event) { emsl.safeMetrics.Lock() defer emsl.safeMetrics.Unlock() emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))] += len(events) emsl.Successes += len(events) } func (emsl *endpointMetricsHTTPStatusListener) failure(status int, events ...Event) { emsl.safeMetrics.Lock() defer emsl.safeMetrics.Unlock() emsl.Statuses[fmt.Sprintf("%d %s", status, http.StatusText(status))] += len(events) emsl.Failures += len(events) } func (emsl *endpointMetricsHTTPStatusListener) err(err error, events ...Event) { emsl.safeMetrics.Lock() defer emsl.safeMetrics.Unlock() emsl.Errors += len(events) } // endpointMetricsEventQueueListener maintains the incoming events counter and // the queues pending count. type endpointMetricsEventQueueListener struct { *safeMetrics } func (eqc *endpointMetricsEventQueueListener) ingress(events ...Event) { eqc.Lock() defer eqc.Unlock() eqc.Events += len(events) eqc.Pending += len(events) } func (eqc *endpointMetricsEventQueueListener) egress(events ...Event) { eqc.Lock() defer eqc.Unlock() eqc.Pending -= len(events) } // endpoints is global registry of endpoints used to report metrics to expvar var endpoints struct { registered []*Endpoint mu sync.Mutex } // register places the endpoint into expvar so that stats are tracked. func register(e *Endpoint) { endpoints.mu.Lock() defer endpoints.mu.Unlock() endpoints.registered = append(endpoints.registered, e) } func init() { // NOTE(stevvooe): Setup registry metrics structure to report to expvar. // Ideally, we do more metrics through logging but we need some nice // realtime metrics for queue state for now. registry := expvar.Get("registry") if registry == nil { registry = expvar.NewMap("registry") } var notifications expvar.Map notifications.Init() notifications.Set("endpoints", expvar.Func(func() interface{} { endpoints.mu.Lock() defer endpoints.mu.Unlock() var names []interface{} for _, v := range endpoints.registered { var epjson struct { Name string `json:"name"` URL string `json:"url"` EndpointConfig Metrics EndpointMetrics } epjson.Name = v.Name() epjson.URL = v.URL() epjson.EndpointConfig = v.EndpointConfig v.ReadMetrics(&epjson.Metrics) names = append(names, epjson) } return names })) registry.(*expvar.Map).Set("notifications", ¬ifications) } docker-registry-2.6.2~ds1/notifications/sinks.go000066400000000000000000000222411313450123100217650ustar00rootroot00000000000000package notifications import ( "container/list" "fmt" "sync" "time" "github.com/Sirupsen/logrus" ) // NOTE(stevvooe): This file contains definitions for several utility sinks. // Typically, the broadcaster is the only sink that should be required // externally, but others are suitable for export if the need arises. Albeit, // the tight integration with endpoint metrics should be removed. // Broadcaster sends events to multiple, reliable Sinks. The goal of this // component is to dispatch events to configured endpoints. Reliability can be // provided by wrapping incoming sinks. type Broadcaster struct { sinks []Sink events chan []Event closed chan chan struct{} } // NewBroadcaster ... // Add appends one or more sinks to the list of sinks. The broadcaster // behavior will be affected by the properties of the sink. Generally, the // sink should accept all messages and deal with reliability on its own. Use // of EventQueue and RetryingSink should be used here. func NewBroadcaster(sinks ...Sink) *Broadcaster { b := Broadcaster{ sinks: sinks, events: make(chan []Event), closed: make(chan chan struct{}), } // Start the broadcaster go b.run() return &b } // Write accepts a block of events to be dispatched to all sinks. This method // will never fail and should never block (hopefully!). The caller cedes the // slice memory to the broadcaster and should not modify it after calling // write. func (b *Broadcaster) Write(events ...Event) error { select { case b.events <- events: case <-b.closed: return ErrSinkClosed } return nil } // Close the broadcaster, ensuring that all messages are flushed to the // underlying sink before returning. func (b *Broadcaster) Close() error { logrus.Infof("broadcaster: closing") select { case <-b.closed: // already closed return fmt.Errorf("broadcaster: already closed") default: // do a little chan handoff dance to synchronize closing closed := make(chan struct{}) b.closed <- closed close(b.closed) <-closed return nil } } // run is the main broadcast loop, started when the broadcaster is created. // Under normal conditions, it waits for events on the event channel. After // Close is called, this goroutine will exit. func (b *Broadcaster) run() { for { select { case block := <-b.events: for _, sink := range b.sinks { if err := sink.Write(block...); err != nil { logrus.Errorf("broadcaster: error writing events to %v, these events will be lost: %v", sink, err) } } case closing := <-b.closed: // close all the underlying sinks for _, sink := range b.sinks { if err := sink.Close(); err != nil { logrus.Errorf("broadcaster: error closing sink %v: %v", sink, err) } } closing <- struct{}{} logrus.Debugf("broadcaster: closed") return } } } // eventQueue accepts all messages into a queue for asynchronous consumption // by a sink. It is unbounded and thread safe but the sink must be reliable or // events will be dropped. type eventQueue struct { sink Sink events *list.List listeners []eventQueueListener cond *sync.Cond mu sync.Mutex closed bool } // eventQueueListener is called when various events happen on the queue. type eventQueueListener interface { ingress(events ...Event) egress(events ...Event) } // newEventQueue returns a queue to the provided sink. If the updater is non- // nil, it will be called to update pending metrics on ingress and egress. func newEventQueue(sink Sink, listeners ...eventQueueListener) *eventQueue { eq := eventQueue{ sink: sink, events: list.New(), listeners: listeners, } eq.cond = sync.NewCond(&eq.mu) go eq.run() return &eq } // Write accepts the events into the queue, only failing if the queue has // beend closed. func (eq *eventQueue) Write(events ...Event) error { eq.mu.Lock() defer eq.mu.Unlock() if eq.closed { return ErrSinkClosed } for _, listener := range eq.listeners { listener.ingress(events...) } eq.events.PushBack(events) eq.cond.Signal() // signal waiters return nil } // Close shutsdown the event queue, flushing func (eq *eventQueue) Close() error { eq.mu.Lock() defer eq.mu.Unlock() if eq.closed { return fmt.Errorf("eventqueue: already closed") } // set closed flag eq.closed = true eq.cond.Signal() // signal flushes queue eq.cond.Wait() // wait for signal from last flush return eq.sink.Close() } // run is the main goroutine to flush events to the target sink. func (eq *eventQueue) run() { for { block := eq.next() if block == nil { return // nil block means event queue is closed. } if err := eq.sink.Write(block...); err != nil { logrus.Warnf("eventqueue: error writing events to %v, these events will be lost: %v", eq.sink, err) } for _, listener := range eq.listeners { listener.egress(block...) } } } // next encompasses the critical section of the run loop. When the queue is // empty, it will block on the condition. If new data arrives, it will wake // and return a block. When closed, a nil slice will be returned. func (eq *eventQueue) next() []Event { eq.mu.Lock() defer eq.mu.Unlock() for eq.events.Len() < 1 { if eq.closed { eq.cond.Broadcast() return nil } eq.cond.Wait() } front := eq.events.Front() block := front.Value.([]Event) eq.events.Remove(front) return block } // ignoredMediaTypesSink discards events with ignored target media types and // passes the rest along. type ignoredMediaTypesSink struct { Sink ignored map[string]bool } func newIgnoredMediaTypesSink(sink Sink, ignored []string) Sink { if len(ignored) == 0 { return sink } ignoredMap := make(map[string]bool) for _, mediaType := range ignored { ignoredMap[mediaType] = true } return &ignoredMediaTypesSink{ Sink: sink, ignored: ignoredMap, } } // Write discards events with ignored target media types and passes the rest // along. func (imts *ignoredMediaTypesSink) Write(events ...Event) error { var kept []Event for _, e := range events { if !imts.ignored[e.Target.MediaType] { kept = append(kept, e) } } if len(kept) == 0 { return nil } return imts.Sink.Write(kept...) } // retryingSink retries the write until success or an ErrSinkClosed is // returned. Underlying sink must have p > 0 of succeeding or the sink will // block. Internally, it is a circuit breaker retries to manage reset. // Concurrent calls to a retrying sink are serialized through the sink, // meaning that if one is in-flight, another will not proceed. type retryingSink struct { mu sync.Mutex sink Sink closed bool // circuit breaker heuristics failures struct { threshold int recent int last time.Time backoff time.Duration // time after which we retry after failure. } } type retryingSinkListener interface { active(events ...Event) retry(events ...Event) } // TODO(stevvooe): We are using circuit break here, which actually doesn't // make a whole lot of sense for this use case, since we always retry. Move // this to use bounded exponential backoff. // newRetryingSink returns a sink that will retry writes to a sink, backing // off on failure. Parameters threshold and backoff adjust the behavior of the // circuit breaker. func newRetryingSink(sink Sink, threshold int, backoff time.Duration) *retryingSink { rs := &retryingSink{ sink: sink, } rs.failures.threshold = threshold rs.failures.backoff = backoff return rs } // Write attempts to flush the events to the downstream sink until it succeeds // or the sink is closed. func (rs *retryingSink) Write(events ...Event) error { rs.mu.Lock() defer rs.mu.Unlock() retry: if rs.closed { return ErrSinkClosed } if !rs.proceed() { logrus.Warnf("%v encountered too many errors, backing off", rs.sink) rs.wait(rs.failures.backoff) goto retry } if err := rs.write(events...); err != nil { if err == ErrSinkClosed { // terminal! return err } logrus.Errorf("retryingsink: error writing events: %v, retrying", err) goto retry } return nil } // Close closes the sink and the underlying sink. func (rs *retryingSink) Close() error { rs.mu.Lock() defer rs.mu.Unlock() if rs.closed { return fmt.Errorf("retryingsink: already closed") } rs.closed = true return rs.sink.Close() } // write provides a helper that dispatches failure and success properly. Used // by write as the single-flight write call. func (rs *retryingSink) write(events ...Event) error { if err := rs.sink.Write(events...); err != nil { rs.failure() return err } rs.reset() return nil } // wait backoff time against the sink, unlocking so others can proceed. Should // only be called by methods that currently have the mutex. func (rs *retryingSink) wait(backoff time.Duration) { rs.mu.Unlock() defer rs.mu.Lock() // backoff here time.Sleep(backoff) } // reset marks a successful call. func (rs *retryingSink) reset() { rs.failures.recent = 0 rs.failures.last = time.Time{} } // failure records a failure. func (rs *retryingSink) failure() { rs.failures.recent++ rs.failures.last = time.Now().UTC() } // proceed returns true if the call should proceed based on circuit breaker // heuristics. func (rs *retryingSink) proceed() bool { return rs.failures.recent < rs.failures.threshold || time.Now().UTC().After(rs.failures.last.Add(rs.failures.backoff)) } docker-registry-2.6.2~ds1/notifications/sinks_test.go000066400000000000000000000122561313450123100230310ustar00rootroot00000000000000package notifications import ( "fmt" "math/rand" "reflect" "sync" "time" "github.com/Sirupsen/logrus" "testing" ) func TestBroadcaster(t *testing.T) { const nEvents = 1000 var sinks []Sink for i := 0; i < 10; i++ { sinks = append(sinks, &testSink{}) } b := NewBroadcaster(sinks...) var block []Event var wg sync.WaitGroup for i := 1; i <= nEvents; i++ { block = append(block, createTestEvent("push", "library/test", "blob")) if i%10 == 0 && i > 0 { wg.Add(1) go func(block ...Event) { if err := b.Write(block...); err != nil { t.Fatalf("error writing block of length %d: %v", len(block), err) } wg.Done() }(block...) block = nil } } wg.Wait() // Wait until writes complete checkClose(t, b) // Iterate through the sinks and check that they all have the expected length. for _, sink := range sinks { ts := sink.(*testSink) ts.mu.Lock() defer ts.mu.Unlock() if len(ts.events) != nEvents { t.Fatalf("not all events ended up in testsink: len(testSink) == %d, not %d", len(ts.events), nEvents) } if !ts.closed { t.Fatalf("sink should have been closed") } } } func TestEventQueue(t *testing.T) { const nevents = 1000 var ts testSink metrics := newSafeMetrics() eq := newEventQueue( // delayed sync simulates destination slower than channel comms &delayedSink{ Sink: &ts, delay: time.Millisecond * 1, }, metrics.eventQueueListener()) var wg sync.WaitGroup var block []Event for i := 1; i <= nevents; i++ { block = append(block, createTestEvent("push", "library/test", "blob")) if i%10 == 0 && i > 0 { wg.Add(1) go func(block ...Event) { if err := eq.Write(block...); err != nil { t.Fatalf("error writing event block: %v", err) } wg.Done() }(block...) block = nil } } wg.Wait() checkClose(t, eq) ts.mu.Lock() defer ts.mu.Unlock() metrics.Lock() defer metrics.Unlock() if len(ts.events) != nevents { t.Fatalf("events did not make it to the sink: %d != %d", len(ts.events), 1000) } if !ts.closed { t.Fatalf("sink should have been closed") } if metrics.Events != nevents { t.Fatalf("unexpected ingress count: %d != %d", metrics.Events, nevents) } if metrics.Pending != 0 { t.Fatalf("unexpected egress count: %d != %d", metrics.Pending, 0) } } func TestIgnoredMediaTypesSink(t *testing.T) { blob := createTestEvent("push", "library/test", "blob") manifest := createTestEvent("push", "library/test", "manifest") type testcase struct { ignored []string expected []Event } cases := []testcase{ {nil, []Event{blob, manifest}}, {[]string{"other"}, []Event{blob, manifest}}, {[]string{"blob"}, []Event{manifest}}, {[]string{"blob", "manifest"}, nil}, } for _, c := range cases { ts := &testSink{} s := newIgnoredMediaTypesSink(ts, c.ignored) if err := s.Write(blob, manifest); err != nil { t.Fatalf("error writing event: %v", err) } ts.mu.Lock() if !reflect.DeepEqual(ts.events, c.expected) { t.Fatalf("unexpected events: %#v != %#v", ts.events, c.expected) } ts.mu.Unlock() } } func TestRetryingSink(t *testing.T) { // Make a sync that fails most of the time, ensuring that all the events // make it through. var ts testSink flaky := &flakySink{ rate: 1.0, // start out always failing. Sink: &ts, } s := newRetryingSink(flaky, 3, 10*time.Millisecond) var wg sync.WaitGroup var block []Event for i := 1; i <= 100; i++ { block = append(block, createTestEvent("push", "library/test", "blob")) // Above 50, set the failure rate lower if i > 50 { s.mu.Lock() flaky.rate = 0.90 s.mu.Unlock() } if i%10 == 0 && i > 0 { wg.Add(1) go func(block ...Event) { defer wg.Done() if err := s.Write(block...); err != nil { t.Fatalf("error writing event block: %v", err) } }(block...) block = nil } } wg.Wait() checkClose(t, s) ts.mu.Lock() defer ts.mu.Unlock() if len(ts.events) != 100 { t.Fatalf("events not propagated: %d != %d", len(ts.events), 100) } } type testSink struct { events []Event mu sync.Mutex closed bool } func (ts *testSink) Write(events ...Event) error { ts.mu.Lock() defer ts.mu.Unlock() ts.events = append(ts.events, events...) return nil } func (ts *testSink) Close() error { ts.mu.Lock() defer ts.mu.Unlock() ts.closed = true logrus.Infof("closing testSink") return nil } type delayedSink struct { Sink delay time.Duration } func (ds *delayedSink) Write(events ...Event) error { time.Sleep(ds.delay) return ds.Sink.Write(events...) } type flakySink struct { Sink rate float64 } func (fs *flakySink) Write(events ...Event) error { if rand.Float64() < fs.rate { return fmt.Errorf("error writing %d events", len(events)) } return fs.Sink.Write(events...) } func checkClose(t *testing.T, sink Sink) { if err := sink.Close(); err != nil { t.Fatalf("unexpected error closing: %v", err) } // second close should not crash but should return an error. if err := sink.Close(); err == nil { t.Fatalf("no error on double close") } // Write after closed should be an error if err := sink.Write([]Event{}...); err == nil { t.Fatalf("write after closed did not have an error") } else if err != ErrSinkClosed { t.Fatalf("error should be ErrSinkClosed") } } docker-registry-2.6.2~ds1/project/000077500000000000000000000000001313450123100171035ustar00rootroot00000000000000docker-registry-2.6.2~ds1/project/dev-image/000077500000000000000000000000001313450123100207415ustar00rootroot00000000000000docker-registry-2.6.2~ds1/project/dev-image/Dockerfile000066400000000000000000000011271313450123100227340ustar00rootroot00000000000000FROM ubuntu:14.04 ENV GOLANG_VERSION 1.4rc1 ENV GOPATH /var/cache/drone ENV GOROOT /usr/local/go ENV PATH $PATH:$GOROOT/bin:$GOPATH/bin ENV LANG C ENV LC_ALL C RUN apt-get update && apt-get install -y \ wget ca-certificates git mercurial bzr \ --no-install-recommends \ && rm -rf /var/lib/apt/lists/* RUN wget https://golang.org/dl/go$GOLANG_VERSION.linux-amd64.tar.gz --quiet && \ tar -C /usr/local -xzf go$GOLANG_VERSION.linux-amd64.tar.gz && \ rm go${GOLANG_VERSION}.linux-amd64.tar.gz RUN go get github.com/axw/gocov/gocov github.com/mattn/goveralls github.com/golang/lint/golint docker-registry-2.6.2~ds1/project/hooks/000077500000000000000000000000001313450123100202265ustar00rootroot00000000000000docker-registry-2.6.2~ds1/project/hooks/README.md000066400000000000000000000011751313450123100215110ustar00rootroot00000000000000Git Hooks ========= To enforce valid and properly-formatted code, there is CI in place which runs `gofmt`, `golint`, and `go vet` against code in the repository. As an aid to prevent committing invalid code in the first place, a git pre-commit hook has been added to the repository, found in [pre-commit](./pre-commit). As it is impossible to automatically add linked hooks to a git repository, this hook should be linked into your `.git/hooks/pre-commit`, which can be done by running the `configure-hooks.sh` script in this directory. This script is the preferred method of configuring hooks, as it will be updated as more are added.docker-registry-2.6.2~ds1/project/hooks/configure-hooks.sh000077500000000000000000000006351313450123100236730ustar00rootroot00000000000000#!/bin/sh cd $(dirname $0) REPO_ROOT=$(git rev-parse --show-toplevel) RESOLVE_REPO_ROOT_STATUS=$? if [ "$RESOLVE_REPO_ROOT_STATUS" -ne "0" ]; then echo -e "Unable to resolve repository root. Error:\n$REPO_ROOT" > /dev/stderr exit $RESOLVE_REPO_ROOT_STATUS fi set -e set -x # Just in case the directory doesn't exist mkdir -p $REPO_ROOT/.git/hooks ln -f -s $(pwd)/pre-commit $REPO_ROOT/.git/hooks/pre-commitdocker-registry-2.6.2~ds1/project/hooks/pre-commit000077500000000000000000000015701313450123100222330ustar00rootroot00000000000000#!/bin/sh REPO_ROOT=$(git rev-parse --show-toplevel) RESOLVE_REPO_ROOT_STATUS=$? if [ "$RESOLVE_REPO_ROOT_STATUS" -ne "0" ]; then printf "Unable to resolve repository root. Error:\n%s\n" "$RESOLVE_REPO_ROOT_STATUS" > /dev/stderr exit $RESOLVE_REPO_ROOT_STATUS fi cd $REPO_ROOT GOFMT_ERRORS=$(gofmt -s -l . 2>&1) if [ -n "$GOFMT_ERRORS" ]; then printf 'gofmt failed for the following files:\n%s\n\nPlease run "gofmt -s -l ." in the root of your repository before committing\n' "$GOFMT_ERRORS" > /dev/stderr exit 1 fi GOLINT_ERRORS=$(golint ./... 2>&1) if [ -n "$GOLINT_ERRORS" ]; then printf "golint failed with the following errors:\n%s\n" "$GOLINT_ERRORS" > /dev/stderr exit 1 fi GOVET_ERRORS=$(go vet ./... 2>&1) GOVET_STATUS=$? if [ "$GOVET_STATUS" -ne "0" ]; then printf "govet failed with the following errors:\n%s\n" "$GOVET_ERRORS" > /dev/stderr exit $GOVET_STATUS fi docker-registry-2.6.2~ds1/reference/000077500000000000000000000000001313450123100173735ustar00rootroot00000000000000docker-registry-2.6.2~ds1/reference/reference.go000066400000000000000000000225641313450123100216710ustar00rootroot00000000000000// Package reference provides a general type to represent any way of referencing images within the registry. // Its main purpose is to abstract tags and digests (content-addressable hash). // // Grammar // // reference := name [ ":" tag ] [ "@" digest ] // name := [hostname '/'] component ['/' component]* // hostname := hostcomponent ['.' hostcomponent]* [':' port-number] // hostcomponent := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/ // port-number := /[0-9]+/ // component := alpha-numeric [separator alpha-numeric]* // alpha-numeric := /[a-z0-9]+/ // separator := /[_.]|__|[-]*/ // // tag := /[\w][\w.-]{0,127}/ // // digest := digest-algorithm ":" digest-hex // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ] // digest-algorithm-separator := /[+.-_]/ // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/ // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value package reference import ( "errors" "fmt" "path" "strings" "github.com/docker/distribution/digest" ) const ( // NameTotalLengthMax is the maximum total number of characters in a repository name. NameTotalLengthMax = 255 ) var ( // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference. ErrReferenceInvalidFormat = errors.New("invalid reference format") // ErrTagInvalidFormat represents an error while trying to parse a string as a tag. ErrTagInvalidFormat = errors.New("invalid tag format") // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag. ErrDigestInvalidFormat = errors.New("invalid digest format") // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters. ErrNameContainsUppercase = errors.New("repository name must be lowercase") // ErrNameEmpty is returned for empty, invalid repository names. ErrNameEmpty = errors.New("repository name must have at least one component") // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax. ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax) ) // Reference is an opaque object reference identifier that may include // modifiers such as a hostname, name, tag, and digest. type Reference interface { // String returns the full reference String() string } // Field provides a wrapper type for resolving correct reference types when // working with encoding. type Field struct { reference Reference } // AsField wraps a reference in a Field for encoding. func AsField(reference Reference) Field { return Field{reference} } // Reference unwraps the reference type from the field to // return the Reference object. This object should be // of the appropriate type to further check for different // reference types. func (f Field) Reference() Reference { return f.reference } // MarshalText serializes the field to byte text which // is the string of the reference. func (f Field) MarshalText() (p []byte, err error) { return []byte(f.reference.String()), nil } // UnmarshalText parses text bytes by invoking the // reference parser to ensure the appropriately // typed reference object is wrapped by field. func (f *Field) UnmarshalText(p []byte) error { r, err := Parse(string(p)) if err != nil { return err } f.reference = r return nil } // Named is an object with a full name type Named interface { Reference Name() string } // Tagged is an object which has a tag type Tagged interface { Reference Tag() string } // NamedTagged is an object including a name and tag. type NamedTagged interface { Named Tag() string } // Digested is an object which has a digest // in which it can be referenced by type Digested interface { Reference Digest() digest.Digest } // Canonical reference is an object with a fully unique // name including a name with hostname and digest type Canonical interface { Named Digest() digest.Digest } // SplitHostname splits a named reference into a // hostname and name string. If no valid hostname is // found, the hostname is empty and the full value // is returned as name func SplitHostname(named Named) (string, string) { name := named.Name() match := anchoredNameRegexp.FindStringSubmatch(name) if len(match) != 3 { return "", name } return match[1], match[2] } // Parse parses s and returns a syntactically valid Reference. // If an error was encountered it is returned, along with a nil Reference. // NOTE: Parse will not handle short digests. func Parse(s string) (Reference, error) { matches := ReferenceRegexp.FindStringSubmatch(s) if matches == nil { if s == "" { return nil, ErrNameEmpty } if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil { return nil, ErrNameContainsUppercase } return nil, ErrReferenceInvalidFormat } if len(matches[1]) > NameTotalLengthMax { return nil, ErrNameTooLong } ref := reference{ name: matches[1], tag: matches[2], } if matches[3] != "" { var err error ref.digest, err = digest.ParseDigest(matches[3]) if err != nil { return nil, err } } r := getBestReferenceType(ref) if r == nil { return nil, ErrNameEmpty } return r, nil } // ParseNamed parses s and returns a syntactically valid reference implementing // the Named interface. The reference must have a name, otherwise an error is // returned. // If an error was encountered it is returned, along with a nil Reference. // NOTE: ParseNamed will not handle short digests. func ParseNamed(s string) (Named, error) { ref, err := Parse(s) if err != nil { return nil, err } named, isNamed := ref.(Named) if !isNamed { return nil, fmt.Errorf("reference %s has no name", ref.String()) } return named, nil } // WithName returns a named object representing the given string. If the input // is invalid ErrReferenceInvalidFormat will be returned. func WithName(name string) (Named, error) { if len(name) > NameTotalLengthMax { return nil, ErrNameTooLong } if !anchoredNameRegexp.MatchString(name) { return nil, ErrReferenceInvalidFormat } return repository(name), nil } // WithTag combines the name from "name" and the tag from "tag" to form a // reference incorporating both the name and the tag. func WithTag(name Named, tag string) (NamedTagged, error) { if !anchoredTagRegexp.MatchString(tag) { return nil, ErrTagInvalidFormat } if canonical, ok := name.(Canonical); ok { return reference{ name: name.Name(), tag: tag, digest: canonical.Digest(), }, nil } return taggedReference{ name: name.Name(), tag: tag, }, nil } // WithDigest combines the name from "name" and the digest from "digest" to form // a reference incorporating both the name and the digest. func WithDigest(name Named, digest digest.Digest) (Canonical, error) { if !anchoredDigestRegexp.MatchString(digest.String()) { return nil, ErrDigestInvalidFormat } if tagged, ok := name.(Tagged); ok { return reference{ name: name.Name(), tag: tagged.Tag(), digest: digest, }, nil } return canonicalReference{ name: name.Name(), digest: digest, }, nil } // Match reports whether ref matches the specified pattern. // See https://godoc.org/path#Match for supported patterns. func Match(pattern string, ref Reference) (bool, error) { matched, err := path.Match(pattern, ref.String()) if namedRef, isNamed := ref.(Named); isNamed && !matched { matched, _ = path.Match(pattern, namedRef.Name()) } return matched, err } // TrimNamed removes any tag or digest from the named reference. func TrimNamed(ref Named) Named { return repository(ref.Name()) } func getBestReferenceType(ref reference) Reference { if ref.name == "" { // Allow digest only references if ref.digest != "" { return digestReference(ref.digest) } return nil } if ref.tag == "" { if ref.digest != "" { return canonicalReference{ name: ref.name, digest: ref.digest, } } return repository(ref.name) } if ref.digest == "" { return taggedReference{ name: ref.name, tag: ref.tag, } } return ref } type reference struct { name string tag string digest digest.Digest } func (r reference) String() string { return r.name + ":" + r.tag + "@" + r.digest.String() } func (r reference) Name() string { return r.name } func (r reference) Tag() string { return r.tag } func (r reference) Digest() digest.Digest { return r.digest } type repository string func (r repository) String() string { return string(r) } func (r repository) Name() string { return string(r) } type digestReference digest.Digest func (d digestReference) String() string { return d.String() } func (d digestReference) Digest() digest.Digest { return digest.Digest(d) } type taggedReference struct { name string tag string } func (t taggedReference) String() string { return t.name + ":" + t.tag } func (t taggedReference) Name() string { return t.name } func (t taggedReference) Tag() string { return t.tag } type canonicalReference struct { name string digest digest.Digest } func (c canonicalReference) String() string { return c.name + "@" + c.digest.String() } func (c canonicalReference) Name() string { return c.name } func (c canonicalReference) Digest() digest.Digest { return c.digest } docker-registry-2.6.2~ds1/reference/reference_test.go000066400000000000000000000402511313450123100227210ustar00rootroot00000000000000package reference import ( "encoding/json" "strconv" "strings" "testing" "github.com/docker/distribution/digest" ) func TestReferenceParse(t *testing.T) { // referenceTestcases is a unified set of testcases for // testing the parsing of references referenceTestcases := []struct { // input is the repository name or name component testcase input string // err is the error expected from Parse, or nil err error // repository is the string representation for the reference repository string // hostname is the hostname expected in the reference hostname string // tag is the tag for the reference tag string // digest is the digest for the reference (enforces digest reference) digest string }{ { input: "test_com", repository: "test_com", }, { input: "test.com:tag", repository: "test.com", tag: "tag", }, { input: "test.com:5000", repository: "test.com", tag: "5000", }, { input: "test.com/repo:tag", hostname: "test.com", repository: "test.com/repo", tag: "tag", }, { input: "test:5000/repo", hostname: "test:5000", repository: "test:5000/repo", }, { input: "test:5000/repo:tag", hostname: "test:5000", repository: "test:5000/repo", tag: "tag", }, { input: "test:5000/repo@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", hostname: "test:5000", repository: "test:5000/repo", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "test:5000/repo:tag@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", hostname: "test:5000", repository: "test:5000/repo", tag: "tag", digest: "sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "test:5000/repo", hostname: "test:5000", repository: "test:5000/repo", }, { input: "", err: ErrNameEmpty, }, { input: ":justtag", err: ErrReferenceInvalidFormat, }, { input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: "repo@sha256:ffffffffffffffffffffffffffffffffff", err: digest.ErrDigestInvalidLength, }, { input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: digest.ErrDigestUnsupported, }, { input: "Uppercase:tag", err: ErrNameContainsUppercase, }, // FIXME "Uppercase" is incorrectly handled as a domain-name here, therefore passes. // See https://github.com/docker/distribution/pull/1778, and https://github.com/docker/docker/pull/20175 //{ // input: "Uppercase/lowercase:tag", // err: ErrNameContainsUppercase, //}, { input: "test:5000/Uppercase/lowercase:tag", err: ErrNameContainsUppercase, }, { input: "lowercase:Uppercase", repository: "lowercase", tag: "Uppercase", }, { input: strings.Repeat("a/", 128) + "a:tag", err: ErrNameTooLong, }, { input: strings.Repeat("a/", 127) + "a:tag-puts-this-over-max", hostname: "a", repository: strings.Repeat("a/", 127) + "a", tag: "tag-puts-this-over-max", }, { input: "aa/asdf$$^/aa", err: ErrReferenceInvalidFormat, }, { input: "sub-dom1.foo.com/bar/baz/quux", hostname: "sub-dom1.foo.com", repository: "sub-dom1.foo.com/bar/baz/quux", }, { input: "sub-dom1.foo.com/bar/baz/quux:some-long-tag", hostname: "sub-dom1.foo.com", repository: "sub-dom1.foo.com/bar/baz/quux", tag: "some-long-tag", }, { input: "b.gcr.io/test.example.com/my-app:test.example.com", hostname: "b.gcr.io", repository: "b.gcr.io/test.example.com/my-app", tag: "test.example.com", }, { input: "xn--n3h.com/myimage:xn--n3h.com", // ☃.com in punycode hostname: "xn--n3h.com", repository: "xn--n3h.com/myimage", tag: "xn--n3h.com", }, { input: "xn--7o8h.com/myimage:xn--7o8h.com@sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", // 🐳.com in punycode hostname: "xn--7o8h.com", repository: "xn--7o8h.com/myimage", tag: "xn--7o8h.com", digest: "sha512:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", }, { input: "foo_bar.com:8080", repository: "foo_bar.com", tag: "8080", }, { input: "foo/foo_bar.com:8080", hostname: "foo", repository: "foo/foo_bar.com", tag: "8080", }, } for _, testcase := range referenceTestcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Fail() } repo, err := Parse(testcase.input) if testcase.err != nil { if err == nil { failf("missing expected error: %v", testcase.err) } else if testcase.err != err { failf("mismatched error: got %v, expected %v", err, testcase.err) } continue } else if err != nil { failf("unexpected parse error: %v", err) continue } if repo.String() != testcase.input { failf("mismatched repo: got %q, expected %q", repo.String(), testcase.input) } if named, ok := repo.(Named); ok { if named.Name() != testcase.repository { failf("unexpected repository: got %q, expected %q", named.Name(), testcase.repository) } hostname, _ := SplitHostname(named) if hostname != testcase.hostname { failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) } } else if testcase.repository != "" || testcase.hostname != "" { failf("expected named type, got %T", repo) } tagged, ok := repo.(Tagged) if testcase.tag != "" { if ok { if tagged.Tag() != testcase.tag { failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag) } } else { failf("expected tagged type, got %T", repo) } } else if ok { failf("unexpected tagged type") } digested, ok := repo.(Digested) if testcase.digest != "" { if ok { if digested.Digest().String() != testcase.digest { failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest) } } else { failf("expected digested type, got %T", repo) } } else if ok { failf("unexpected digested type") } } } // TestWithNameFailure tests cases where WithName should fail. Cases where it // should succeed are covered by TestSplitHostname, below. func TestWithNameFailure(t *testing.T) { testcases := []struct { input string err error }{ { input: "", err: ErrNameEmpty, }, { input: ":justtag", err: ErrReferenceInvalidFormat, }, { input: "@sha256:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: "validname@invaliddigest:ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", err: ErrReferenceInvalidFormat, }, { input: strings.Repeat("a/", 128) + "a:tag", err: ErrNameTooLong, }, { input: "aa/asdf$$^/aa", err: ErrReferenceInvalidFormat, }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Fail() } _, err := WithName(testcase.input) if err == nil { failf("no error parsing name. expected: %s", testcase.err) } } } func TestSplitHostname(t *testing.T) { testcases := []struct { input string hostname string name string }{ { input: "test.com/foo", hostname: "test.com", name: "foo", }, { input: "test_com/foo", hostname: "", name: "test_com/foo", }, { input: "test:8080/foo", hostname: "test:8080", name: "foo", }, { input: "test.com:8080/foo", hostname: "test.com:8080", name: "foo", }, { input: "test-com:8080/foo", hostname: "test-com:8080", name: "foo", }, { input: "xn--n3h.com:18080/foo", hostname: "xn--n3h.com:18080", name: "foo", }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Fail() } named, err := WithName(testcase.input) if err != nil { failf("error parsing name: %s", err) } hostname, name := SplitHostname(named) if hostname != testcase.hostname { failf("unexpected hostname: got %q, expected %q", hostname, testcase.hostname) } if name != testcase.name { failf("unexpected name: got %q, expected %q", name, testcase.name) } } } type serializationType struct { Description string Field Field } func TestSerialization(t *testing.T) { testcases := []struct { description string input string name string tag string digest string err error }{ { description: "empty value", err: ErrNameEmpty, }, { description: "just a name", input: "example.com:8000/named", name: "example.com:8000/named", }, { description: "name with a tag", input: "example.com:8000/named:tagged", name: "example.com:8000/named", tag: "tagged", }, { description: "name with digest", input: "other.com/named@sha256:1234567890098765432112345667890098765432112345667890098765432112", name: "other.com/named", digest: "sha256:1234567890098765432112345667890098765432112345667890098765432112", }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.input)+": "+format, v...) t.Fail() } m := map[string]string{ "Description": testcase.description, "Field": testcase.input, } b, err := json.Marshal(m) if err != nil { failf("error marshalling: %v", err) } t := serializationType{} if err := json.Unmarshal(b, &t); err != nil { if testcase.err == nil { failf("error unmarshalling: %v", err) } if err != testcase.err { failf("wrong error, expected %v, got %v", testcase.err, err) } continue } else if testcase.err != nil { failf("expected error unmarshalling: %v", testcase.err) } if t.Description != testcase.description { failf("wrong description, expected %q, got %q", testcase.description, t.Description) } ref := t.Field.Reference() if named, ok := ref.(Named); ok { if named.Name() != testcase.name { failf("unexpected repository: got %q, expected %q", named.Name(), testcase.name) } } else if testcase.name != "" { failf("expected named type, got %T", ref) } tagged, ok := ref.(Tagged) if testcase.tag != "" { if ok { if tagged.Tag() != testcase.tag { failf("unexpected tag: got %q, expected %q", tagged.Tag(), testcase.tag) } } else { failf("expected tagged type, got %T", ref) } } else if ok { failf("unexpected tagged type") } digested, ok := ref.(Digested) if testcase.digest != "" { if ok { if digested.Digest().String() != testcase.digest { failf("unexpected digest: got %q, expected %q", digested.Digest().String(), testcase.digest) } } else { failf("expected digested type, got %T", ref) } } else if ok { failf("unexpected digested type") } t = serializationType{ Description: testcase.description, Field: AsField(ref), } b2, err := json.Marshal(t) if err != nil { failf("error marshing serialization type: %v", err) } if string(b) != string(b2) { failf("unexpected serialized value: expected %q, got %q", string(b), string(b2)) } // Ensure t.Field is not implementing "Reference" directly, getting // around the Reference type system var fieldInterface interface{} = t.Field if _, ok := fieldInterface.(Reference); ok { failf("field should not implement Reference interface") } } } func TestWithTag(t *testing.T) { testcases := []struct { name string digest digest.Digest tag string combined string }{ { name: "test.com/foo", tag: "tag", combined: "test.com/foo:tag", }, { name: "foo", tag: "tag2", combined: "foo:tag2", }, { name: "test.com:8000/foo", tag: "tag4", combined: "test.com:8000/foo:tag4", }, { name: "test.com:8000/foo", tag: "TAG5", combined: "test.com:8000/foo:TAG5", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", tag: "TAG5", combined: "test.com:8000/foo:TAG5@sha256:1234567890098765432112345667890098765", }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.name)+": "+format, v...) t.Fail() } named, err := WithName(testcase.name) if err != nil { failf("error parsing name: %s", err) } if testcase.digest != "" { canonical, err := WithDigest(named, testcase.digest) if err != nil { failf("error adding digest") } named = canonical } tagged, err := WithTag(named, testcase.tag) if err != nil { failf("WithTag failed: %s", err) } if tagged.String() != testcase.combined { failf("unexpected: got %q, expected %q", tagged.String(), testcase.combined) } } } func TestWithDigest(t *testing.T) { testcases := []struct { name string digest digest.Digest tag string combined string }{ { name: "test.com/foo", digest: "sha256:1234567890098765432112345667890098765", combined: "test.com/foo@sha256:1234567890098765432112345667890098765", }, { name: "foo", digest: "sha256:1234567890098765432112345667890098765", combined: "foo@sha256:1234567890098765432112345667890098765", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", combined: "test.com:8000/foo@sha256:1234567890098765432112345667890098765", }, { name: "test.com:8000/foo", digest: "sha256:1234567890098765432112345667890098765", tag: "latest", combined: "test.com:8000/foo:latest@sha256:1234567890098765432112345667890098765", }, } for _, testcase := range testcases { failf := func(format string, v ...interface{}) { t.Logf(strconv.Quote(testcase.name)+": "+format, v...) t.Fail() } named, err := WithName(testcase.name) if err != nil { failf("error parsing name: %s", err) } if testcase.tag != "" { tagged, err := WithTag(named, testcase.tag) if err != nil { failf("error adding tag") } named = tagged } digested, err := WithDigest(named, testcase.digest) if err != nil { failf("WithDigest failed: %s", err) } if digested.String() != testcase.combined { failf("unexpected: got %q, expected %q", digested.String(), testcase.combined) } } } func TestMatchError(t *testing.T) { named, err := Parse("foo") if err != nil { t.Fatal(err) } _, err = Match("[-x]", named) if err == nil { t.Fatalf("expected an error, got nothing") } } func TestMatch(t *testing.T) { matchCases := []struct { reference string pattern string expected bool }{ { reference: "foo", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/any/bat", pattern: "foo/**/ba[rz]", expected: false, }, { reference: "foo/a/bar", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/b/baz", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/**/ba[rz]", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/*/baz:tag", expected: true, }, { reference: "foo/c/baz:tag", pattern: "foo/c/baz:tag", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "*/foo/c/baz", expected: true, }, { reference: "example.com/foo/c/baz:tag", pattern: "example.com/foo/c/baz", expected: true, }, } for _, c := range matchCases { named, err := Parse(c.reference) if err != nil { t.Fatal(err) } actual, err := Match(c.pattern, named) if err != nil { t.Fatal(err) } if actual != c.expected { t.Fatalf("expected %s match %s to be %v, was %v", c.reference, c.pattern, c.expected, actual) } } } docker-registry-2.6.2~ds1/reference/regexp.go000066400000000000000000000105231313450123100212150ustar00rootroot00000000000000package reference import "regexp" var ( // alphaNumericRegexp defines the alpha numeric atom, typically a // component of names. This only allows lower case characters and digits. alphaNumericRegexp = match(`[a-z0-9]+`) // separatorRegexp defines the separators allowed to be embedded in name // components. This allow one period, one or two underscore and multiple // dashes. separatorRegexp = match(`(?:[._]|__|[-]*)`) // nameComponentRegexp restricts registry path component names to start // with at least one letter or number, with following parts able to be // separated by one period, one or two underscore and multiple dashes. nameComponentRegexp = expression( alphaNumericRegexp, optional(repeated(separatorRegexp, alphaNumericRegexp))) // hostnameComponentRegexp restricts the registry hostname component of a // repository name to start with a component as defined by hostnameRegexp // and followed by an optional port. hostnameComponentRegexp = match(`(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])`) // hostnameRegexp defines the structure of potential hostname components // that may be part of image names. This is purposely a subset of what is // allowed by DNS to ensure backwards compatibility with Docker image // names. hostnameRegexp = expression( hostnameComponentRegexp, optional(repeated(literal(`.`), hostnameComponentRegexp)), optional(literal(`:`), match(`[0-9]+`))) // TagRegexp matches valid tag names. From docker/docker:graph/tags.go. TagRegexp = match(`[\w][\w.-]{0,127}`) // anchoredTagRegexp matches valid tag names, anchored at the start and // end of the matched string. anchoredTagRegexp = anchored(TagRegexp) // DigestRegexp matches valid digests. DigestRegexp = match(`[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}`) // anchoredDigestRegexp matches valid digests, anchored at the start and // end of the matched string. anchoredDigestRegexp = anchored(DigestRegexp) // NameRegexp is the format for the name component of references. The // regexp has capturing groups for the hostname and name part omitting // the separating forward slash from either. NameRegexp = expression( optional(hostnameRegexp, literal(`/`)), nameComponentRegexp, optional(repeated(literal(`/`), nameComponentRegexp))) // anchoredNameRegexp is used to parse a name value, capturing the // hostname and trailing components. anchoredNameRegexp = anchored( optional(capture(hostnameRegexp), literal(`/`)), capture(nameComponentRegexp, optional(repeated(literal(`/`), nameComponentRegexp)))) // ReferenceRegexp is the full supported format of a reference. The regexp // is anchored and has capturing groups for name, tag, and digest // components. ReferenceRegexp = anchored(capture(NameRegexp), optional(literal(":"), capture(TagRegexp)), optional(literal("@"), capture(DigestRegexp))) ) // match compiles the string to a regular expression. var match = regexp.MustCompile // literal compiles s into a literal regular expression, escaping any regexp // reserved characters. func literal(s string) *regexp.Regexp { re := match(regexp.QuoteMeta(s)) if _, complete := re.LiteralPrefix(); !complete { panic("must be a literal") } return re } // expression defines a full expression, where each regular expression must // follow the previous. func expression(res ...*regexp.Regexp) *regexp.Regexp { var s string for _, re := range res { s += re.String() } return match(s) } // optional wraps the expression in a non-capturing group and makes the // production optional. func optional(res ...*regexp.Regexp) *regexp.Regexp { return match(group(expression(res...)).String() + `?`) } // repeated wraps the regexp in a non-capturing group to get one or more // matches. func repeated(res ...*regexp.Regexp) *regexp.Regexp { return match(group(expression(res...)).String() + `+`) } // group wraps the regexp in a non-capturing group. func group(res ...*regexp.Regexp) *regexp.Regexp { return match(`(?:` + expression(res...).String() + `)`) } // capture wraps the expression in a capturing group. func capture(res ...*regexp.Regexp) *regexp.Regexp { return match(`(` + expression(res...).String() + `)`) } // anchored anchors the regular expression by adding start and end delimiters. func anchored(res ...*regexp.Regexp) *regexp.Regexp { return match(`^` + expression(res...).String() + `$`) } docker-registry-2.6.2~ds1/reference/regexp_test.go000066400000000000000000000231751313450123100222630ustar00rootroot00000000000000package reference import ( "regexp" "strings" "testing" ) type regexpMatch struct { input string match bool subs []string } func checkRegexp(t *testing.T, r *regexp.Regexp, m regexpMatch) { matches := r.FindStringSubmatch(m.input) if m.match && matches != nil { if len(matches) != (r.NumSubexp()+1) || matches[0] != m.input { t.Fatalf("Bad match result %#v for %q", matches, m.input) } if len(matches) < (len(m.subs) + 1) { t.Errorf("Expected %d sub matches, only have %d for %q", len(m.subs), len(matches)-1, m.input) } for i := range m.subs { if m.subs[i] != matches[i+1] { t.Errorf("Unexpected submatch %d: %q, expected %q for %q", i+1, matches[i+1], m.subs[i], m.input) } } } else if m.match { t.Errorf("Expected match for %q", m.input) } else if matches != nil { t.Errorf("Unexpected match for %q", m.input) } } func TestHostRegexp(t *testing.T) { hostcases := []regexpMatch{ { input: "test.com", match: true, }, { input: "test.com:10304", match: true, }, { input: "test.com:http", match: false, }, { input: "localhost", match: true, }, { input: "localhost:8080", match: true, }, { input: "a", match: true, }, { input: "a.b", match: true, }, { input: "ab.cd.com", match: true, }, { input: "a-b.com", match: true, }, { input: "-ab.com", match: false, }, { input: "ab-.com", match: false, }, { input: "ab.c-om", match: true, }, { input: "ab.-com", match: false, }, { input: "ab.com-", match: false, }, { input: "0101.com", match: true, // TODO(dmcgowan): valid if this should be allowed }, { input: "001a.com", match: true, }, { input: "b.gbc.io:443", match: true, }, { input: "b.gbc.io", match: true, }, { input: "xn--n3h.com", // ☃.com in punycode match: true, }, { input: "Asdf.com", // uppercase character match: true, }, } r := regexp.MustCompile(`^` + hostnameRegexp.String() + `$`) for i := range hostcases { checkRegexp(t, r, hostcases[i]) } } func TestFullNameRegexp(t *testing.T) { if anchoredNameRegexp.NumSubexp() != 2 { t.Fatalf("anchored name regexp should have two submatches: %v, %v != 2", anchoredNameRegexp, anchoredNameRegexp.NumSubexp()) } testcases := []regexpMatch{ { input: "", match: false, }, { input: "short", match: true, subs: []string{"", "short"}, }, { input: "simple/name", match: true, subs: []string{"simple", "name"}, }, { input: "library/ubuntu", match: true, subs: []string{"library", "ubuntu"}, }, { input: "docker/stevvooe/app", match: true, subs: []string{"docker", "stevvooe/app"}, }, { input: "aa/aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb", match: true, subs: []string{"aa", "aa/aa/aa/aa/aa/aa/aa/aa/bb/bb/bb/bb/bb/bb"}, }, { input: "aa/aa/bb/bb/bb", match: true, subs: []string{"aa", "aa/bb/bb/bb"}, }, { input: "a/a/a/a", match: true, subs: []string{"a", "a/a/a"}, }, { input: "a/a/a/a/", match: false, }, { input: "a//a/a", match: false, }, { input: "a", match: true, subs: []string{"", "a"}, }, { input: "a/aa", match: true, subs: []string{"a", "aa"}, }, { input: "a/aa/a", match: true, subs: []string{"a", "aa/a"}, }, { input: "foo.com", match: true, subs: []string{"", "foo.com"}, }, { input: "foo.com/", match: false, }, { input: "foo.com:8080/bar", match: true, subs: []string{"foo.com:8080", "bar"}, }, { input: "foo.com:http/bar", match: false, }, { input: "foo.com/bar", match: true, subs: []string{"foo.com", "bar"}, }, { input: "foo.com/bar/baz", match: true, subs: []string{"foo.com", "bar/baz"}, }, { input: "localhost:8080/bar", match: true, subs: []string{"localhost:8080", "bar"}, }, { input: "sub-dom1.foo.com/bar/baz/quux", match: true, subs: []string{"sub-dom1.foo.com", "bar/baz/quux"}, }, { input: "blog.foo.com/bar/baz", match: true, subs: []string{"blog.foo.com", "bar/baz"}, }, { input: "a^a", match: false, }, { input: "aa/asdf$$^/aa", match: false, }, { input: "asdf$$^/aa", match: false, }, { input: "aa-a/a", match: true, subs: []string{"aa-a", "a"}, }, { input: strings.Repeat("a/", 128) + "a", match: true, subs: []string{"a", strings.Repeat("a/", 127) + "a"}, }, { input: "a-/a/a/a", match: false, }, { input: "foo.com/a-/a/a", match: false, }, { input: "-foo/bar", match: false, }, { input: "foo/bar-", match: false, }, { input: "foo-/bar", match: false, }, { input: "foo/-bar", match: false, }, { input: "_foo/bar", match: false, }, { input: "foo_bar", match: true, subs: []string{"", "foo_bar"}, }, { input: "foo_bar.com", match: true, subs: []string{"", "foo_bar.com"}, }, { input: "foo_bar.com:8080", match: false, }, { input: "foo_bar.com:8080/app", match: false, }, { input: "foo.com/foo_bar", match: true, subs: []string{"foo.com", "foo_bar"}, }, { input: "____/____", match: false, }, { input: "_docker/_docker", match: false, }, { input: "docker_/docker_", match: false, }, { input: "b.gcr.io/test.example.com/my-app", match: true, subs: []string{"b.gcr.io", "test.example.com/my-app"}, }, { input: "xn--n3h.com/myimage", // ☃.com in punycode match: true, subs: []string{"xn--n3h.com", "myimage"}, }, { input: "xn--7o8h.com/myimage", // 🐳.com in punycode match: true, subs: []string{"xn--7o8h.com", "myimage"}, }, { input: "example.com/xn--7o8h.com/myimage", // 🐳.com in punycode match: true, subs: []string{"example.com", "xn--7o8h.com/myimage"}, }, { input: "example.com/some_separator__underscore/myimage", match: true, subs: []string{"example.com", "some_separator__underscore/myimage"}, }, { input: "example.com/__underscore/myimage", match: false, }, { input: "example.com/..dots/myimage", match: false, }, { input: "example.com/.dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "example.com/nodouble..dots/myimage", match: false, }, { input: "docker./docker", match: false, }, { input: ".docker/docker", match: false, }, { input: "docker-/docker", match: false, }, { input: "-docker/docker", match: false, }, { input: "do..cker/docker", match: false, }, { input: "do__cker:8080/docker", match: false, }, { input: "do__cker/docker", match: true, subs: []string{"", "do__cker/docker"}, }, { input: "b.gcr.io/test.example.com/my-app", match: true, subs: []string{"b.gcr.io", "test.example.com/my-app"}, }, { input: "registry.io/foo/project--id.module--name.ver---sion--name", match: true, subs: []string{"registry.io", "foo/project--id.module--name.ver---sion--name"}, }, { input: "Asdf.com/foo/bar", // uppercase character in hostname match: true, }, { input: "Foo/FarB", // uppercase characters in remote name match: false, }, } for i := range testcases { checkRegexp(t, anchoredNameRegexp, testcases[i]) } } func TestReferenceRegexp(t *testing.T) { if ReferenceRegexp.NumSubexp() != 3 { t.Fatalf("anchored name regexp should have three submatches: %v, %v != 3", ReferenceRegexp, ReferenceRegexp.NumSubexp()) } testcases := []regexpMatch{ { input: "registry.com:8080/myapp:tag", match: true, subs: []string{"registry.com:8080/myapp", "tag", ""}, }, { input: "registry.com:8080/myapp@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"registry.com:8080/myapp", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp:tag2@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"registry.com:8080/myapp", "tag2", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp@sha256:badbadbadbad", match: false, }, { input: "registry.com:8080/myapp:invalid~tag", match: false, }, { input: "bad_hostname.com:8080/myapp:tag", match: false, }, { input:// localhost treated as name, missing tag with 8080 as tag "localhost:8080@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost", "8080", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "localhost:8080/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost:8080/name", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "localhost:http/name@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: false, }, { // localhost will be treated as an image name without a host input: "localhost@sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912", match: true, subs: []string{"localhost", "", "sha256:be178c0543eb17f5f3043021c9e5fcf30285e557a4fc309cce97ff9ca6182912"}, }, { input: "registry.com:8080/myapp@bad", match: false, }, { input: "registry.com:8080/myapp@2bad", match: false, // TODO(dmcgowan): Support this as valid }, } for i := range testcases { checkRegexp(t, ReferenceRegexp, testcases[i]) } } docker-registry-2.6.2~ds1/registry.go000066400000000000000000000064341313450123100176430ustar00rootroot00000000000000package distribution import ( "github.com/docker/distribution/context" "github.com/docker/distribution/reference" ) // Scope defines the set of items that match a namespace. type Scope interface { // Contains returns true if the name belongs to the namespace. Contains(name string) bool } type fullScope struct{} func (f fullScope) Contains(string) bool { return true } // GlobalScope represents the full namespace scope which contains // all other scopes. var GlobalScope = Scope(fullScope{}) // Namespace represents a collection of repositories, addressable by name. // Generally, a namespace is backed by a set of one or more services, // providing facilities such as registry access, trust, and indexing. type Namespace interface { // Scope describes the names that can be used with this Namespace. The // global namespace will have a scope that matches all names. The scope // effectively provides an identity for the namespace. Scope() Scope // Repository should return a reference to the named repository. The // registry may or may not have the repository but should always return a // reference. Repository(ctx context.Context, name reference.Named) (Repository, error) // Repositories fills 'repos' with a lexigraphically sorted catalog of repositories // up to the size of 'repos' and returns the value 'n' for the number of entries // which were filled. 'last' contains an offset in the catalog, and 'err' will be // set to io.EOF if there are no more entries to obtain. Repositories(ctx context.Context, repos []string, last string) (n int, err error) // Blobs returns a blob enumerator to access all blobs Blobs() BlobEnumerator // BlobStatter returns a BlobStatter to control BlobStatter() BlobStatter } // RepositoryEnumerator describes an operation to enumerate repositories type RepositoryEnumerator interface { Enumerate(ctx context.Context, ingester func(string) error) error } // ManifestServiceOption is a function argument for Manifest Service methods type ManifestServiceOption interface { Apply(ManifestService) error } // WithTag allows a tag to be passed into Put func WithTag(tag string) ManifestServiceOption { return WithTagOption{tag} } // WithTagOption holds a tag type WithTagOption struct{ Tag string } // Apply conforms to the ManifestServiceOption interface func (o WithTagOption) Apply(m ManifestService) error { // no implementation return nil } // Repository is a named collection of manifests and layers. type Repository interface { // Named returns the name of the repository. Named() reference.Named // Manifests returns a reference to this repository's manifest service. // with the supplied options applied. Manifests(ctx context.Context, options ...ManifestServiceOption) (ManifestService, error) // Blobs returns a reference to this repository's blob service. Blobs(ctx context.Context) BlobStore // TODO(stevvooe): The above BlobStore return can probably be relaxed to // be a BlobService for use with clients. This will allow such // implementations to avoid implementing ServeBlob. // Tags returns a reference to this repositories tag service Tags(ctx context.Context) TagService } // TODO(stevvooe): Must add close methods to all these. May want to change the // way instances are created to better reflect internal dependency // relationships. docker-registry-2.6.2~ds1/registry/000077500000000000000000000000001313450123100173055ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/api/000077500000000000000000000000001313450123100200565ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/api/errcode/000077500000000000000000000000001313450123100215015ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/api/errcode/errors.go000066400000000000000000000150411313450123100233450ustar00rootroot00000000000000package errcode import ( "encoding/json" "fmt" "strings" ) // ErrorCoder is the base interface for ErrorCode and Error allowing // users of each to just call ErrorCode to get the real ID of each type ErrorCoder interface { ErrorCode() ErrorCode } // ErrorCode represents the error type. The errors are serialized via strings // and the integer format may change and should *never* be exported. type ErrorCode int var _ error = ErrorCode(0) // ErrorCode just returns itself func (ec ErrorCode) ErrorCode() ErrorCode { return ec } // Error returns the ID/Value func (ec ErrorCode) Error() string { // NOTE(stevvooe): Cannot use message here since it may have unpopulated args. return strings.ToLower(strings.Replace(ec.String(), "_", " ", -1)) } // Descriptor returns the descriptor for the error code. func (ec ErrorCode) Descriptor() ErrorDescriptor { d, ok := errorCodeToDescriptors[ec] if !ok { return ErrorCodeUnknown.Descriptor() } return d } // String returns the canonical identifier for this error code. func (ec ErrorCode) String() string { return ec.Descriptor().Value } // Message returned the human-readable error message for this error code. func (ec ErrorCode) Message() string { return ec.Descriptor().Message } // MarshalText encodes the receiver into UTF-8-encoded text and returns the // result. func (ec ErrorCode) MarshalText() (text []byte, err error) { return []byte(ec.String()), nil } // UnmarshalText decodes the form generated by MarshalText. func (ec *ErrorCode) UnmarshalText(text []byte) error { desc, ok := idToDescriptors[string(text)] if !ok { desc = ErrorCodeUnknown.Descriptor() } *ec = desc.Code return nil } // WithMessage creates a new Error struct based on the passed-in info and // overrides the Message property. func (ec ErrorCode) WithMessage(message string) Error { return Error{ Code: ec, Message: message, } } // WithDetail creates a new Error struct based on the passed-in info and // set the Detail property appropriately func (ec ErrorCode) WithDetail(detail interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithDetail(detail) } // WithArgs creates a new Error struct and sets the Args slice func (ec ErrorCode) WithArgs(args ...interface{}) Error { return Error{ Code: ec, Message: ec.Message(), }.WithArgs(args...) } // Error provides a wrapper around ErrorCode with extra Details provided. type Error struct { Code ErrorCode `json:"code"` Message string `json:"message"` Detail interface{} `json:"detail,omitempty"` // TODO(duglin): See if we need an "args" property so we can do the // variable substitution right before showing the message to the user } var _ error = Error{} // ErrorCode returns the ID/Value of this Error func (e Error) ErrorCode() ErrorCode { return e.Code } // Error returns a human readable representation of the error. func (e Error) Error() string { return fmt.Sprintf("%s: %s", e.Code.Error(), e.Message) } // WithDetail will return a new Error, based on the current one, but with // some Detail info added func (e Error) WithDetail(detail interface{}) Error { return Error{ Code: e.Code, Message: e.Message, Detail: detail, } } // WithArgs uses the passed-in list of interface{} as the substitution // variables in the Error's Message string, but returns a new Error func (e Error) WithArgs(args ...interface{}) Error { return Error{ Code: e.Code, Message: fmt.Sprintf(e.Code.Message(), args...), Detail: e.Detail, } } // 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 // HTTPStatusCode provides the http status code that is associated with // this error condition. HTTPStatusCode int } // ParseErrorCode returns the value by the string error code. // `ErrorCodeUnknown` will be returned if the error is not known. func ParseErrorCode(value string) ErrorCode { ed, ok := idToDescriptors[value] if ok { return ed.Code } return ErrorCodeUnknown } // Errors provides the envelope for multiple errors and a few sugar methods // for use within the application. type Errors []error var _ error = Errors{} func (errs Errors) Error() string { switch len(errs) { case 0: return "" case 1: return errs[0].Error() default: msg := "errors:\n" for _, err := range errs { msg += err.Error() + "\n" } return msg } } // Len returns the current number of errors. func (errs Errors) Len() int { return len(errs) } // MarshalJSON converts slice of error, ErrorCode or Error into a // slice of Error - then serializes func (errs Errors) MarshalJSON() ([]byte, error) { var tmpErrs struct { Errors []Error `json:"errors,omitempty"` } for _, daErr := range errs { var err Error switch daErr.(type) { case ErrorCode: err = daErr.(ErrorCode).WithDetail(nil) case Error: err = daErr.(Error) default: err = ErrorCodeUnknown.WithDetail(daErr) } // If the Error struct was setup and they forgot to set the // Message field (meaning its "") then grab it from the ErrCode msg := err.Message if msg == "" { msg = err.Code.Message() } tmpErrs.Errors = append(tmpErrs.Errors, Error{ Code: err.Code, Message: msg, Detail: err.Detail, }) } return json.Marshal(tmpErrs) } // UnmarshalJSON deserializes []Error and then converts it into slice of // Error or ErrorCode func (errs *Errors) UnmarshalJSON(data []byte) error { var tmpErrs struct { Errors []Error } if err := json.Unmarshal(data, &tmpErrs); err != nil { return err } var newErrs Errors for _, daErr := range tmpErrs.Errors { // If Message is empty or exactly matches the Code's message string // then just use the Code, no need for a full Error struct if daErr.Detail == nil && (daErr.Message == "" || daErr.Message == daErr.Code.Message()) { // Error's w/o details get converted to ErrorCode newErrs = append(newErrs, daErr.Code) } else { // Error's w/ details are untouched newErrs = append(newErrs, Error{ Code: daErr.Code, Message: daErr.Message, Detail: daErr.Detail, }) } } *errs = newErrs return nil } docker-registry-2.6.2~ds1/registry/api/errcode/errors_test.go000066400000000000000000000132061313450123100244050ustar00rootroot00000000000000package errcode import ( "encoding/json" "net/http" "reflect" "strings" "testing" ) // TestErrorsManagement does a quick check of the Errors type to ensure that // members are properly pushed and marshaled. var ErrorCodeTest1 = Register("test.errors", ErrorDescriptor{ Value: "TEST1", Message: "test error 1", Description: `Just a test message #1.`, HTTPStatusCode: http.StatusInternalServerError, }) var ErrorCodeTest2 = Register("test.errors", ErrorDescriptor{ Value: "TEST2", Message: "test error 2", Description: `Just a test message #2.`, HTTPStatusCode: http.StatusNotFound, }) var ErrorCodeTest3 = Register("test.errors", ErrorDescriptor{ Value: "TEST3", Message: "Sorry %q isn't valid", Description: `Just a test message #3.`, HTTPStatusCode: http.StatusNotFound, }) // TestErrorCodes ensures that error code format, mappings and // marshaling/unmarshaling. round trips are stable. func TestErrorCodes(t *testing.T) { if len(errorCodeToDescriptors) == 0 { t.Fatal("errors aren't loaded!") } for ec, desc := range errorCodeToDescriptors { if ec != desc.Code { t.Fatalf("error code in descriptor isn't correct, %q != %q", ec, desc.Code) } if idToDescriptors[desc.Value].Code != ec { t.Fatalf("error code in idToDesc isn't correct, %q != %q", idToDescriptors[desc.Value].Code, ec) } if ec.Message() != desc.Message { t.Fatalf("ec.Message doesn't mtach desc.Message: %q != %q", ec.Message(), desc.Message) } // Test (de)serializing the ErrorCode p, err := json.Marshal(ec) if err != nil { t.Fatalf("couldn't marshal ec %v: %v", ec, err) } if len(p) <= 0 { t.Fatalf("expected content in marshaled before for error code %v", ec) } // First, unmarshal to interface and ensure we have a string. var ecUnspecified interface{} if err := json.Unmarshal(p, &ecUnspecified); err != nil { t.Fatalf("error unmarshaling error code %v: %v", ec, err) } if _, ok := ecUnspecified.(string); !ok { t.Fatalf("expected a string for error code %v on unmarshal got a %T", ec, ecUnspecified) } // Now, unmarshal with the error code type and ensure they are equal var ecUnmarshaled ErrorCode if err := json.Unmarshal(p, &ecUnmarshaled); err != nil { t.Fatalf("error unmarshaling error code %v: %v", ec, err) } if ecUnmarshaled != ec { t.Fatalf("unexpected error code during error code marshal/unmarshal: %v != %v", ecUnmarshaled, ec) } expectedErrorString := strings.ToLower(strings.Replace(ec.Descriptor().Value, "_", " ", -1)) if ec.Error() != expectedErrorString { t.Fatalf("unexpected return from %v.Error(): %q != %q", ec, ec.Error(), expectedErrorString) } } } func TestErrorsManagement(t *testing.T) { var errs Errors errs = append(errs, ErrorCodeTest1) errs = append(errs, ErrorCodeTest2.WithDetail( map[string]interface{}{"digest": "sometestblobsumdoesntmatter"})) errs = append(errs, ErrorCodeTest3.WithArgs("BOOGIE")) errs = append(errs, ErrorCodeTest3.WithArgs("BOOGIE").WithDetail("data")) p, err := json.Marshal(errs) if err != nil { t.Fatalf("error marashaling errors: %v", err) } expectedJSON := `{"errors":[` + `{"code":"TEST1","message":"test error 1"},` + `{"code":"TEST2","message":"test error 2","detail":{"digest":"sometestblobsumdoesntmatter"}},` + `{"code":"TEST3","message":"Sorry \"BOOGIE\" isn't valid"},` + `{"code":"TEST3","message":"Sorry \"BOOGIE\" isn't valid","detail":"data"}` + `]}` if string(p) != expectedJSON { t.Fatalf("unexpected json:\ngot:\n%q\n\nexpected:\n%q", string(p), expectedJSON) } // Now test the reverse var unmarshaled Errors if err := json.Unmarshal(p, &unmarshaled); err != nil { t.Fatalf("unexpected error unmarshaling error envelope: %v", err) } if !reflect.DeepEqual(unmarshaled, errs) { t.Fatalf("errors not equal after round trip:\nunmarshaled:\n%#v\n\nerrs:\n%#v", unmarshaled, errs) } // Test the arg substitution stuff e1 := unmarshaled[3].(Error) exp1 := `Sorry "BOOGIE" isn't valid` if e1.Message != exp1 { t.Fatalf("Wrong msg, got:\n%q\n\nexpected:\n%q", e1.Message, exp1) } exp1 = "test3: " + exp1 if e1.Error() != exp1 { t.Fatalf("Error() didn't return the right string, got:%s\nexpected:%s", e1.Error(), exp1) } // Test again with a single value this time errs = Errors{ErrorCodeUnknown} expectedJSON = "{\"errors\":[{\"code\":\"UNKNOWN\",\"message\":\"unknown error\"}]}" p, err = json.Marshal(errs) if err != nil { t.Fatalf("error marashaling errors: %v", err) } if string(p) != expectedJSON { t.Fatalf("unexpected json: %q != %q", string(p), expectedJSON) } // Now test the reverse unmarshaled = nil if err := json.Unmarshal(p, &unmarshaled); err != nil { t.Fatalf("unexpected error unmarshaling error envelope: %v", err) } if !reflect.DeepEqual(unmarshaled, errs) { t.Fatalf("errors not equal after round trip:\nunmarshaled:\n%#v\n\nerrs:\n%#v", unmarshaled, errs) } // Verify that calling WithArgs() more than once does the right thing. // Meaning creates a new Error and uses the ErrorCode Message e1 = ErrorCodeTest3.WithArgs("test1") e2 := e1.WithArgs("test2") if &e1 == &e2 { t.Fatalf("args: e2 and e1 should not be the same, but they are") } if e2.Message != `Sorry "test2" isn't valid` { t.Fatalf("e2 had wrong message: %q", e2.Message) } // Verify that calling WithDetail() more than once does the right thing. // Meaning creates a new Error and overwrites the old detail field e1 = ErrorCodeTest3.WithDetail("stuff1") e2 = e1.WithDetail("stuff2") if &e1 == &e2 { t.Fatalf("detail: e2 and e1 should not be the same, but they are") } if e2.Detail != `stuff2` { t.Fatalf("e2 had wrong detail: %q", e2.Detail) } } docker-registry-2.6.2~ds1/registry/api/errcode/handler.go000066400000000000000000000017441313450123100234530ustar00rootroot00000000000000package errcode import ( "encoding/json" "net/http" ) // ServeJSON attempts to serve the errcode in a JSON envelope. It marshals err // and sets the content-type header to 'application/json'. It will handle // ErrorCoder and Errors, and if necessary will create an envelope. func ServeJSON(w http.ResponseWriter, err error) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") var sc int switch errs := err.(type) { case Errors: if len(errs) < 1 { break } if err, ok := errs[0].(ErrorCoder); ok { sc = err.ErrorCode().Descriptor().HTTPStatusCode } case ErrorCoder: sc = errs.ErrorCode().Descriptor().HTTPStatusCode err = Errors{err} // create an envelope. default: // We just have an unhandled error type, so just place in an envelope // and move along. err = Errors{err} } if sc == 0 { sc = http.StatusInternalServerError } w.WriteHeader(sc) if err := json.NewEncoder(w).Encode(err); err != nil { return err } return nil } docker-registry-2.6.2~ds1/registry/api/errcode/register.go000066400000000000000000000105021313450123100236520ustar00rootroot00000000000000package errcode import ( "fmt" "net/http" "sort" "sync" ) var ( errorCodeToDescriptors = map[ErrorCode]ErrorDescriptor{} idToDescriptors = map[string]ErrorDescriptor{} groupToDescriptors = map[string][]ErrorDescriptor{} ) var ( // ErrorCodeUnknown is a generic error that can be used as a last // resort if there is no situation-specific error message that can be used ErrorCodeUnknown = Register("errcode", ErrorDescriptor{ Value: "UNKNOWN", Message: "unknown error", Description: `Generic error returned when the error does not have an API classification.`, HTTPStatusCode: http.StatusInternalServerError, }) // ErrorCodeUnsupported is returned when an operation is not supported. ErrorCodeUnsupported = Register("errcode", ErrorDescriptor{ Value: "UNSUPPORTED", Message: "The operation is unsupported.", Description: `The operation was unsupported due to a missing implementation or invalid set of parameters.`, HTTPStatusCode: http.StatusMethodNotAllowed, }) // ErrorCodeUnauthorized is returned if a request requires // authentication. ErrorCodeUnauthorized = Register("errcode", ErrorDescriptor{ Value: "UNAUTHORIZED", Message: "authentication required", Description: `The access controller was unable to authenticate the client. Often this will be accompanied by a Www-Authenticate HTTP response header indicating how to authenticate.`, HTTPStatusCode: http.StatusUnauthorized, }) // ErrorCodeDenied is returned if a client does not have sufficient // permission to perform an action. ErrorCodeDenied = Register("errcode", ErrorDescriptor{ Value: "DENIED", Message: "requested access to the resource is denied", Description: `The access controller denied access for the operation on a resource.`, HTTPStatusCode: http.StatusForbidden, }) // ErrorCodeUnavailable provides a common error to report unavailability // of a service or endpoint. ErrorCodeUnavailable = Register("errcode", ErrorDescriptor{ Value: "UNAVAILABLE", Message: "service unavailable", Description: "Returned when a service is not available", HTTPStatusCode: http.StatusServiceUnavailable, }) // ErrorCodeTooManyRequests is returned if a client attempts too many // times to contact a service endpoint. ErrorCodeTooManyRequests = Register("errcode", ErrorDescriptor{ Value: "TOOMANYREQUESTS", Message: "too many requests", Description: `Returned when a client attempts to contact a service too many times`, HTTPStatusCode: http.StatusTooManyRequests, }) ) var nextCode = 1000 var registerLock sync.Mutex // Register will make the passed-in error known to the environment and // return a new ErrorCode func Register(group string, descriptor ErrorDescriptor) ErrorCode { registerLock.Lock() defer registerLock.Unlock() descriptor.Code = ErrorCode(nextCode) if _, ok := idToDescriptors[descriptor.Value]; ok { panic(fmt.Sprintf("ErrorValue %q is already registered", descriptor.Value)) } if _, ok := errorCodeToDescriptors[descriptor.Code]; ok { panic(fmt.Sprintf("ErrorCode %v is already registered", descriptor.Code)) } groupToDescriptors[group] = append(groupToDescriptors[group], descriptor) errorCodeToDescriptors[descriptor.Code] = descriptor idToDescriptors[descriptor.Value] = descriptor nextCode++ return descriptor.Code } type byValue []ErrorDescriptor func (a byValue) Len() int { return len(a) } func (a byValue) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a byValue) Less(i, j int) bool { return a[i].Value < a[j].Value } // GetGroupNames returns the list of Error group names that are registered func GetGroupNames() []string { keys := []string{} for k := range groupToDescriptors { keys = append(keys, k) } sort.Strings(keys) return keys } // GetErrorCodeGroup returns the named group of error descriptors func GetErrorCodeGroup(name string) []ErrorDescriptor { desc := groupToDescriptors[name] sort.Sort(byValue(desc)) return desc } // GetErrorAllDescriptors returns a slice of all ErrorDescriptors that are // registered, irrespective of what group they're in func GetErrorAllDescriptors() []ErrorDescriptor { result := []ErrorDescriptor{} for _, group := range GetGroupNames() { result = append(result, GetErrorCodeGroup(group)...) } sort.Sort(byValue(result)) return result } docker-registry-2.6.2~ds1/registry/api/v2/000077500000000000000000000000001313450123100204055ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/api/v2/descriptors.go000066400000000000000000001460621313450123100233060ustar00rootroot00000000000000package v2 import ( "net/http" "regexp" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" ) var ( nameParameterDescriptor = ParameterDescriptor{ Name: "name", Type: "string", Format: reference.NameRegexp.String(), Required: true, Description: `Name of the target repository.`, } referenceParameterDescriptor = ParameterDescriptor{ Name: "reference", Type: "string", Format: reference.TagRegexp.String(), Required: true, Description: `Tag or digest of the target manifest.`, } uuidParameterDescriptor = ParameterDescriptor{ Name: "uuid", Type: "opaque", Required: true, Description: "A uuid identifying the upload. This field can accept characters that match `[a-zA-Z0-9-_.=]+`.", } digestPathParameter = ParameterDescriptor{ Name: "digest", Type: "path", Required: true, Format: digest.DigestRegexp.String(), Description: `Digest of desired blob.`, } hostHeader = ParameterDescriptor{ Name: "Host", Type: "string", Description: "Standard HTTP Host Header. Should be set to the registry host.", Format: "", Examples: []string{"registry-1.docker.io"}, } authHeader = ParameterDescriptor{ Name: "Authorization", Type: "string", Description: "An RFC7235 compliant authorization header.", Format: " ", Examples: []string{"Bearer dGhpcyBpcyBhIGZha2UgYmVhcmVyIHRva2VuIQ=="}, } authChallengeHeader = ParameterDescriptor{ Name: "WWW-Authenticate", Type: "string", Description: "An RFC7235 compliant authentication challenge header.", Format: ` realm="", ..."`, Examples: []string{ `Bearer realm="https://auth.docker.com/", service="registry.docker.com", scopes="repository:library/ubuntu:pull"`, }, } contentLengthZeroHeader = ParameterDescriptor{ Name: "Content-Length", Description: "The `Content-Length` header must be zero and the body must be empty.", Type: "integer", Format: "0", } dockerUploadUUIDHeader = ParameterDescriptor{ Name: "Docker-Upload-UUID", Description: "Identifies the docker upload uuid for the current request.", Type: "uuid", Format: "", } digestHeader = ParameterDescriptor{ Name: "Docker-Content-Digest", Description: "Digest of the targeted content for the request.", Type: "digest", Format: "", } linkHeader = ParameterDescriptor{ Name: "Link", Type: "link", Description: "RFC5988 compliant rel='next' with URL to next result set, if available", Format: `<?n=&last=>; rel="next"`, } paginationParameters = []ParameterDescriptor{ { Name: "n", Type: "integer", Description: "Limit the number of entries in each response. It not present, all entries will be returned.", Format: "", Required: false, }, { Name: "last", Type: "string", Description: "Result set will include values lexically after last.", Format: "", Required: false, }, } unauthorizedResponseDescriptor = ResponseDescriptor{ Name: "Authentication Required", StatusCode: http.StatusUnauthorized, Description: "The client is not authenticated.", Headers: []ParameterDescriptor{ authChallengeHeader, { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnauthorized, }, } repositoryNotFoundResponseDescriptor = ResponseDescriptor{ Name: "No Such Repository Error", StatusCode: http.StatusNotFound, Description: "The repository is not known to the registry.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, }, } deniedResponseDescriptor = ResponseDescriptor{ Name: "Access Denied", StatusCode: http.StatusForbidden, Description: "The client does not have required access to the repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeDenied, }, } tooManyRequestsDescriptor = ResponseDescriptor{ Name: "Too Many Requests", StatusCode: http.StatusTooManyRequests, Description: "The client made too many requests within a time interval.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeTooManyRequests, }, } ) const ( manifestBody = `{ "name": , "tag": , "fsLayers": [ { "blobSum": "" }, ... ] ], "history": , "signature": }` errorsBody = `{ "errors:" [ { "code": , "message": "", "detail": ... }, ... ] }` ) // APIDescriptor exports descriptions of the layout of the v2 registry API. var APIDescriptor = struct { // RouteDescriptors provides a list of the routes available in the API. RouteDescriptors []RouteDescriptor }{ RouteDescriptors: routeDescriptors, } // RouteDescriptor describes a route specified by name. type RouteDescriptor struct { // Name is the name of the route, as specified in RouteNameXXX exports. // These names a should be considered a unique reference for a route. If // the route is registered with gorilla, this is the name that will be // used. Name string // Path is a gorilla/mux-compatible regexp that can be used to match the // route. For any incoming method and path, only one route descriptor // should match. Path string // Entity should be a short, human-readalbe description of the object // targeted by the endpoint. Entity string // Description should provide an accurate overview of the functionality // provided by the route. Description string // Methods should describe the various HTTP methods that may be used on // this route, including request and response formats. Methods []MethodDescriptor } // MethodDescriptor provides a description of the requests that may be // conducted with the target method. type MethodDescriptor struct { // Method is an HTTP method, such as GET, PUT or POST. Method string // Description should provide an overview of the functionality provided by // the covered method, suitable for use in documentation. Use of markdown // here is encouraged. Description string // Requests is a slice of request descriptors enumerating how this // endpoint may be used. Requests []RequestDescriptor } // RequestDescriptor covers a particular set of headers and parameters that // can be carried out with the parent method. Its most helpful to have one // RequestDescriptor per API use case. type RequestDescriptor struct { // Name provides a short identifier for the request, usable as a title or // to provide quick context for the particular request. Name string // Description should cover the requests purpose, covering any details for // this particular use case. Description string // Headers describes headers that must be used with the HTTP request. Headers []ParameterDescriptor // PathParameters enumerate the parameterized path components for the // given request, as defined in the route's regular expression. PathParameters []ParameterDescriptor // QueryParameters provides a list of query parameters for the given // request. QueryParameters []ParameterDescriptor // Body describes the format of the request body. Body BodyDescriptor // Successes enumerates the possible responses that are considered to be // the result of a successful request. Successes []ResponseDescriptor // Failures covers the possible failures from this particular request. Failures []ResponseDescriptor } // ResponseDescriptor describes the components of an API response. type ResponseDescriptor struct { // Name provides a short identifier for the response, usable as a title or // to provide quick context for the particular response. Name string // Description should provide a brief overview of the role of the // response. Description string // StatusCode specifies the status received by this particular response. StatusCode int // Headers covers any headers that may be returned from the response. Headers []ParameterDescriptor // Fields describes any fields that may be present in the response. Fields []ParameterDescriptor // ErrorCodes enumerates the error codes that may be returned along with // the response. ErrorCodes []errcode.ErrorCode // Body describes the body of the response, if any. Body BodyDescriptor } // BodyDescriptor describes a request body and its expected content type. For // the most part, it should be example json or some placeholder for body // data in documentation. type BodyDescriptor struct { ContentType string Format string } // ParameterDescriptor describes the format of a request parameter, which may // be a header, path parameter or query parameter. type ParameterDescriptor struct { // Name is the name of the parameter, either of the path component or // query parameter. Name string // Type specifies the type of the parameter, such as string, integer, etc. Type string // Description provides a human-readable description of the parameter. Description string // Required means the field is required when set. Required bool // Format is a specifying the string format accepted by this parameter. Format string // Regexp is a compiled regular expression that can be used to validate // the contents of the parameter. Regexp *regexp.Regexp // Examples provides multiple examples for the values that might be valid // for this parameter. Examples []string } var routeDescriptors = []RouteDescriptor{ { Name: RouteNameBase, Path: "/v2/", Entity: "Base", Description: `Base V2 API route. Typically, this can be used for lightweight version checks and to validate registry authentication.`, Methods: []MethodDescriptor{ { Method: "GET", Description: "Check that the endpoint implements Docker Registry API V2.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, Successes: []ResponseDescriptor{ { Description: "The API implements V2 protocol and is accessible.", StatusCode: http.StatusOK, }, }, Failures: []ResponseDescriptor{ { Description: "The registry does not implement the V2 API.", StatusCode: http.StatusNotFound, }, unauthorizedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameTags, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/tags/list", Entity: "Tags", Description: "Retrieve information about tags.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Fetch the tags under the repository identified by `name`.", Requests: []RequestDescriptor{ { Name: "Tags", Description: "Return all tags for the repository", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Description: "A list of tags for the named repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "name": , "tags": [ , ... ] }`, }, }, }, Failures: []ResponseDescriptor{ unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Tags Paginated", Description: "Return a portion of the tags for the specified repository.", PathParameters: []ParameterDescriptor{nameParameterDescriptor}, QueryParameters: paginationParameters, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Description: "A list of tags for the named repository.", Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, linkHeader, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "name": , "tags": [ , ... ], }`, }, }, }, Failures: []ResponseDescriptor{ unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameManifest, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/manifests/{reference:" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + "}", Entity: "Manifest", Description: "Create, update, delete and retrieve manifests.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Fetch the manifest identified by `name` and `reference` where `reference` can be a tag or digest. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Successes: []ResponseDescriptor{ { Description: "The manifest identified by `name` and `reference`. The contents can be used to identify and resolve resources required to run the specified image.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ digestHeader, }, Body: BodyDescriptor{ ContentType: "", Format: manifestBody, }, }, }, Failures: []ResponseDescriptor{ { Description: "The name or reference was invalid.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PUT", Description: "Put the manifest identified by `name` and `reference` where `reference` can be a tag or digest.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Body: BodyDescriptor{ ContentType: "", Format: manifestBody, }, Successes: []ResponseDescriptor{ { Description: "The manifest has been accepted by the registry and is stored under the specified `name` and `tag`.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Description: "The canonical location url of the uploaded manifest.", Format: "", }, contentLengthZeroHeader, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Manifest", Description: "The received manifest was invalid in some way, as described by the error codes. The client should resolve the issue and retry the request.", StatusCode: http.StatusBadRequest, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, ErrorCodeManifestInvalid, ErrorCodeManifestUnverified, ErrorCodeBlobUnknown, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, { Name: "Missing Layer(s)", Description: "One or more layers may be missing during a manifest upload. If so, the missing layers will be enumerated in the error response.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": "" } }, ... ] }`, }, }, { Name: "Not allowed", Description: "Manifest put is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, }, }, }, }, { Method: "DELETE", Description: "Delete the manifest identified by `name` and `reference`. Note that a manifest can _only_ be deleted by `digest`.", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, referenceParameterDescriptor, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusAccepted, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Reference", Description: "The specified `name` or `reference` were invalid and the delete was unable to proceed.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeTagInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, { Name: "Unknown Manifest", Description: "The specified `name` or `reference` are unknown to the registry and the delete was unable to proceed. Clients can assume the manifest was already deleted if this response is returned.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeManifestUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Name: "Not allowed", Description: "Manifest delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled.", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, }, }, }, }, }, }, { Name: RouteNameBlob, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/{digest:" + digest.DigestRegexp.String() + "}", Entity: "Blob", Description: "Operations on blobs identified by `name` and `digest`. Used to fetch or delete layers by digest.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve the blob from the registry identified by `digest`. A `HEAD` request can also be issued to this endpoint to obtain resource information without receiving all data.", Requests: []RequestDescriptor{ { Name: "Fetch Blob", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { Description: "The blob identified by `digest` is available. The blob content will be present in the body of the request.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "The length of the requested blob content.", Format: "", }, digestHeader, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, }, { Description: "The blob identified by `digest` is available at the provided location.", StatusCode: http.StatusTemporaryRedirect, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Description: "The location where the layer should be accessible.", Format: "", }, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeDigestInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The blob, identified by `name` and `digest`, is unknown to the registry.", StatusCode: http.StatusNotFound, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Fetch Blob Part", Description: "This endpoint may also support RFC7233 compliant range requests. Support can be detected by issuing a HEAD request. If the header `Accept-Range: bytes` is returned, range requests can be used to fetch partial content.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Range", Type: "string", Description: "HTTP Range header specifying blob chunk.", Format: "bytes=-", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { Description: "The blob identified by `digest` is available. The specified chunk of blob content will be present in the body of the request.", StatusCode: http.StatusPartialContent, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "The length of the requested blob chunk.", Format: "", }, { Name: "Content-Range", Type: "byte range", Description: "Content range of blob chunk.", Format: "bytes -/", }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, }, }, Failures: []ResponseDescriptor{ { Description: "There was a problem with the request that needs to be addressed by the client, such as an invalid `name` or `tag`.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeDigestInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The range specification cannot be satisfied for the requested content. This can happen when the range is not formatted correctly or if the range is outside of the valid size of the content.", StatusCode: http.StatusRequestedRangeNotSatisfiable, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "DELETE", Description: "Delete the blob identified by `name` and `digest`", Requests: []RequestDescriptor{ { Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, digestPathParameter, }, Successes: []ResponseDescriptor{ { StatusCode: http.StatusAccepted, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "0", Format: "0", }, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Description: "The blob, identified by `name` and `digest`, is unknown to the registry.", StatusCode: http.StatusNotFound, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameUnknown, ErrorCodeBlobUnknown, }, }, { Description: "Blob delete is not allowed because the registry is configured as a pull-through cache or `delete` has been disabled", StatusCode: http.StatusMethodNotAllowed, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, // TODO(stevvooe): We may want to add a PUT request here to // kickoff an upload of a blob, integrated with the blob upload // API. }, }, { Name: RouteNameBlobUpload, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/", Entity: "Initiate Blob Upload", Description: "Initiate a blob upload. This endpoint can be used to create resumable uploads or monolithic uploads.", Methods: []MethodDescriptor{ { Method: "POST", Description: "Initiate a resumable blob upload. If successful, an upload location will be provided to complete the upload. Optionally, if the `digest` parameter is present, the request body will be used to complete the upload in a single request.", Requests: []RequestDescriptor{ { Name: "Initiate Monolithic Blob Upload", Description: "Upload a blob identified by the `digest` parameter in single request. This upload will not be resumable unless a recoverable error is returned.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Length", Type: "integer", Format: "", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "digest", Type: "query", Format: "", Regexp: digest.DigestRegexp, Description: `Digest of uploaded blob. If present, the upload will be completed, in a single request, with contents of the request body as the resulting blob.`, }, }, Body: BodyDescriptor{ ContentType: "application/octect-stream", Format: "", }, Successes: []ResponseDescriptor{ { Description: "The blob has been created in the registry and is available at the provided location.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Name: "Not allowed", Description: "Blob upload is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Initiate Resumable Blob Upload", Description: "Initiate a resumable blob upload with an empty request body.", Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, Successes: []ResponseDescriptor{ { Description: "The upload has been created. The `Location` header must be used to complete the upload. The response should be identical to a `GET` request on the contents of the returned `Location` header.", StatusCode: http.StatusAccepted, Headers: []ParameterDescriptor{ contentLengthZeroHeader, { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the created upload. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Format: "0-0", Description: "Range header indicating the progress of the upload. When starting an upload, it will return an empty range, since no content has been received.", }, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Mount Blob", Description: "Mount a blob identified by the `mount` parameter from another repository.", Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "mount", Type: "query", Format: "", Regexp: digest.DigestRegexp, Description: `Digest of blob to mount from the source repository.`, }, { Name: "from", Type: "query", Format: "", Regexp: reference.NameRegexp, Description: `Name of the source repository.`, }, }, Successes: []ResponseDescriptor{ { Description: "The blob has been mounted in the repository and is available at the provided location.", StatusCode: http.StatusCreated, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Name: "Invalid Name or Digest", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, }, }, { Name: "Not allowed", Description: "Blob mount is not allowed because the registry is configured as a pull-through cache or for some other reason", StatusCode: http.StatusMethodNotAllowed, ErrorCodes: []errcode.ErrorCode{ errcode.ErrorCodeUnsupported, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameBlobUploadChunk, Path: "/v2/{name:" + reference.NameRegexp.String() + "}/blobs/uploads/{uuid:[a-zA-Z0-9-_.=]+}", Entity: "Blob Upload", Description: "Interact with blob uploads. Clients should never assemble URLs for this endpoint and should only take it through the `Location` header on related API requests. The `Location` header and its parameters should be preserved by clients, using the latest value returned via upload related API calls.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve status of upload identified by `uuid`. The primary purpose of this endpoint is to resolve the current status of a resumable upload.", Requests: []RequestDescriptor{ { Description: "Retrieve the progress of the current upload, as reported by the `Range` header.", Headers: []ParameterDescriptor{ hostHeader, authHeader, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Successes: []ResponseDescriptor{ { Name: "Upload Progress", Description: "The upload is known and in progress. The last received offset is available in the `Range` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PATCH", Description: "Upload a chunk of data for the specified upload.", Requests: []RequestDescriptor{ { Name: "Stream upload", Description: "Upload a stream of data to upload without completing the upload.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Data Accepted", Description: "The stream of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, { Name: "Chunked upload", Description: "Upload a chunk of data to specified upload without completing the upload. The data will be uploaded to the specified Content Range.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Range", Type: "header", Format: "-", Required: true, Description: "Range of bytes identifying the desired block of content represented by the body. Start must the end offset retrieved via status check plus one. Note that this is a non-standard use of the `Content-Range` header.", }, { Name: "Content-Length", Type: "integer", Format: "", Description: "Length of the chunk being uploaded, corresponding the length of the request body.", }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Chunk Accepted", Description: "The chunk of data has been accepted and the current progress is available in the range header. The updated upload location is available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "/v2//blobs/uploads/", Description: "The location of the upload. Clients should assume this changes after each request. Clients should use the contents verbatim to complete the upload, adding parameters where required.", }, { Name: "Range", Type: "header", Format: "0-", Description: "Range indicating the current progress of the upload.", }, contentLengthZeroHeader, dockerUploadUUIDHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The `Content-Range` specification cannot be accepted, either because it does not overlap with the current progress or it is invalid.", StatusCode: http.StatusRequestedRangeNotSatisfiable, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "PUT", Description: "Complete the upload specified by `uuid`, optionally appending the body as the final chunk.", Requests: []RequestDescriptor{ { Description: "Complete the upload, providing all the data in the body, if necessary. A request without a body will just complete the upload with previously uploaded content.", Headers: []ParameterDescriptor{ hostHeader, authHeader, { Name: "Content-Length", Type: "integer", Format: "", Description: "Length of the data being uploaded, corresponding to the length of the request body. May be zero if no data is provided.", }, }, PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, QueryParameters: []ParameterDescriptor{ { Name: "digest", Type: "string", Format: "", Regexp: digest.DigestRegexp, Required: true, Description: `Digest of uploaded blob.`, }, }, Body: BodyDescriptor{ ContentType: "application/octet-stream", Format: "", }, Successes: []ResponseDescriptor{ { Name: "Upload Complete", Description: "The upload has been completed and accepted by the registry. The canonical location will be available in the `Location` header.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ { Name: "Location", Type: "url", Format: "", Description: "The canonical location of the blob for retrieval", }, { Name: "Content-Range", Type: "header", Format: "-", Description: "Range of bytes identifying the desired block of content represented by the body. Start must match the end of offset retrieved via status check. Note that this is a non-standard use of the `Content-Range` header.", }, contentLengthZeroHeader, digestHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "There was an error processing the upload and it must be restarted.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeDigestInvalid, ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, errcode.ErrorCodeUnsupported, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The upload must be restarted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, { Method: "DELETE", Description: "Cancel outstanding upload processes, releasing associated resources. If this is not called, the unfinished uploads will eventually timeout.", Requests: []RequestDescriptor{ { Description: "Cancel the upload specified by `uuid`.", PathParameters: []ParameterDescriptor{ nameParameterDescriptor, uuidParameterDescriptor, }, Headers: []ParameterDescriptor{ hostHeader, authHeader, contentLengthZeroHeader, }, Successes: []ResponseDescriptor{ { Name: "Upload Deleted", Description: "The upload has been successfully deleted.", StatusCode: http.StatusNoContent, Headers: []ParameterDescriptor{ contentLengthZeroHeader, }, }, }, Failures: []ResponseDescriptor{ { Description: "An error was encountered processing the delete. The client may ignore this error.", StatusCode: http.StatusBadRequest, ErrorCodes: []errcode.ErrorCode{ ErrorCodeNameInvalid, ErrorCodeBlobUploadInvalid, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, { Description: "The upload is unknown to the registry. The client may ignore this error and assume the upload has been deleted.", StatusCode: http.StatusNotFound, ErrorCodes: []errcode.ErrorCode{ ErrorCodeBlobUploadUnknown, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: errorsBody, }, }, unauthorizedResponseDescriptor, repositoryNotFoundResponseDescriptor, deniedResponseDescriptor, tooManyRequestsDescriptor, }, }, }, }, }, }, { Name: RouteNameCatalog, Path: "/v2/_catalog", Entity: "Catalog", Description: "List a set of available repositories in the local registry cluster. Does not provide any indication of what may be available upstream. Applications can only determine if a repository is available but not if it is not available.", Methods: []MethodDescriptor{ { Method: "GET", Description: "Retrieve a sorted, json list of repositories available in the registry.", Requests: []RequestDescriptor{ { Name: "Catalog Fetch", Description: "Request an unabridged list of repositories available. The implementation may impose a maximum limit and return a partial set with pagination links.", Successes: []ResponseDescriptor{ { Description: "Returns the unabridged list of repositories as a json response.", StatusCode: http.StatusOK, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, }, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "repositories": [ , ... ] }`, }, }, }, }, { Name: "Catalog Fetch Paginated", Description: "Return the specified portion of repositories.", QueryParameters: paginationParameters, Successes: []ResponseDescriptor{ { StatusCode: http.StatusOK, Body: BodyDescriptor{ ContentType: "application/json; charset=utf-8", Format: `{ "repositories": [ , ... ] "next": "?last=&n=" }`, }, Headers: []ParameterDescriptor{ { Name: "Content-Length", Type: "integer", Description: "Length of the JSON response body.", Format: "", }, linkHeader, }, }, }, }, }, }, }, }, } var routeDescriptorsMap map[string]RouteDescriptor func init() { routeDescriptorsMap = make(map[string]RouteDescriptor, len(routeDescriptors)) for _, descriptor := range routeDescriptors { routeDescriptorsMap[descriptor.Name] = descriptor } } docker-registry-2.6.2~ds1/registry/api/v2/doc.go000066400000000000000000000007351313450123100215060ustar00rootroot00000000000000// Package v2 describes routes, urls and the error codes used in the Docker // Registry JSON HTTP API V2. In addition to declarations, descriptors are // provided for routes and error codes that can be used for implementation and // automatically generating documentation. // // 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 // change proposal in docker core. package v2 docker-registry-2.6.2~ds1/registry/api/v2/errors.go000066400000000000000000000127571313450123100222640ustar00rootroot00000000000000package v2 import ( "net/http" "github.com/docker/distribution/registry/api/errcode" ) const errGroup = "registry.api.v2" var ( // ErrorCodeDigestInvalid is returned when uploading a blob if the // provided digest does not match the blob contents. ErrorCodeDigestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeSizeInvalid is returned when uploading a blob if the provided ErrorCodeSizeInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeNameInvalid is returned when the name in the manifest does not // match the provided name. ErrorCodeNameInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "NAME_INVALID", Message: "invalid repository name", Description: `Invalid repository name encountered either during manifest validation or any API operation.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeTagInvalid is returned when the tag in the manifest does not // match the provided tag. ErrorCodeTagInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeNameUnknown when the repository name is not known. ErrorCodeNameUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeManifestUnknown returned when image manifest is unknown. ErrorCodeManifestUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_UNKNOWN", Message: "manifest unknown", Description: `This error is returned when the manifest, identified by name and tag is unknown to the repository.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeManifestInvalid returned when an image manifest is invalid, // typically during a PUT operation. This error encompasses all errors // encountered during manifest validation that aren't signature errors. ErrorCodeManifestInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeManifestUnverified is returned when the manifest fails // signature verification. ErrorCodeManifestUnverified = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_UNVERIFIED", Message: "manifest failed signature verification", Description: `During manifest upload, if the manifest fails signature verification, this error will be returned.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeManifestBlobUnknown is returned when a manifest blob is // unknown to the registry. ErrorCodeManifestBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "MANIFEST_BLOB_UNKNOWN", Message: "blob unknown to registry", Description: `This error may be returned when a manifest blob is unknown to the registry.`, HTTPStatusCode: http.StatusBadRequest, }) // ErrorCodeBlobUnknown is returned when a blob is unknown to the // registry. This can happen when the manifest references a nonexistent // layer or the result is not found by a blob fetch. ErrorCodeBlobUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeBlobUploadUnknown is returned when an upload is unknown. ErrorCodeBlobUploadUnknown = errcode.Register(errGroup, errcode.ErrorDescriptor{ 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.`, HTTPStatusCode: http.StatusNotFound, }) // ErrorCodeBlobUploadInvalid is returned when an upload is invalid. ErrorCodeBlobUploadInvalid = errcode.Register(errGroup, errcode.ErrorDescriptor{ Value: "BLOB_UPLOAD_INVALID", Message: "blob upload invalid", Description: `The blob upload encountered an error and can no longer proceed.`, HTTPStatusCode: http.StatusNotFound, }) ) docker-registry-2.6.2~ds1/registry/api/v2/headerparser.go000066400000000000000000000113741313450123100234070ustar00rootroot00000000000000package v2 import ( "fmt" "regexp" "strings" "unicode" ) var ( // according to rfc7230 reToken = regexp.MustCompile(`^[^"(),/:;<=>?@[\]{}[:space:][:cntrl:]]+`) reQuotedValue = regexp.MustCompile(`^[^\\"]+`) reEscapedCharacter = regexp.MustCompile(`^[[:blank:][:graph:]]`) ) // parseForwardedHeader is a benevolent parser of Forwarded header defined in rfc7239. The header contains // a comma-separated list of forwarding key-value pairs. Each list element is set by single proxy. The // function parses only the first element of the list, which is set by the very first proxy. It returns a map // of corresponding key-value pairs and an unparsed slice of the input string. // // Examples of Forwarded header values: // // 1. Forwarded: For=192.0.2.43; Proto=https,For="[2001:db8:cafe::17]",For=unknown // 2. Forwarded: for="192.0.2.43:443"; host="registry.example.org", for="10.10.05.40:80" // // The first will be parsed into {"for": "192.0.2.43", "proto": "https"} while the second into // {"for": "192.0.2.43:443", "host": "registry.example.org"}. func parseForwardedHeader(forwarded string) (map[string]string, string, error) { // Following are states of forwarded header parser. Any state could transition to a failure. const ( // terminating state; can transition to Parameter stateElement = iota // terminating state; can transition to KeyValueDelimiter stateParameter // can transition to Value stateKeyValueDelimiter // can transition to one of { QuotedValue, PairEnd } stateValue // can transition to one of { EscapedCharacter, PairEnd } stateQuotedValue // can transition to one of { QuotedValue } stateEscapedCharacter // terminating state; can transition to one of { Parameter, Element } statePairEnd ) var ( parameter string value string parse = forwarded[:] res = map[string]string{} state = stateElement ) Loop: for { // skip spaces unless in quoted value if state != stateQuotedValue && state != stateEscapedCharacter { parse = strings.TrimLeftFunc(parse, unicode.IsSpace) } if len(parse) == 0 { if state != stateElement && state != statePairEnd && state != stateParameter { return nil, parse, fmt.Errorf("unexpected end of input") } // terminating break } switch state { // terminate at list element delimiter case stateElement: if parse[0] == ',' { parse = parse[1:] break Loop } state = stateParameter // parse parameter (the key of key-value pair) case stateParameter: match := reToken.FindString(parse) if len(match) == 0 { return nil, parse, fmt.Errorf("failed to parse token at position %d", len(forwarded)-len(parse)) } parameter = strings.ToLower(match) parse = parse[len(match):] state = stateKeyValueDelimiter // parse '=' case stateKeyValueDelimiter: if parse[0] != '=' { return nil, parse, fmt.Errorf("expected '=', not '%c' at position %d", parse[0], len(forwarded)-len(parse)) } parse = parse[1:] state = stateValue // parse value or quoted value case stateValue: if parse[0] == '"' { parse = parse[1:] state = stateQuotedValue } else { value = reToken.FindString(parse) if len(value) == 0 { return nil, parse, fmt.Errorf("failed to parse value at position %d", len(forwarded)-len(parse)) } if _, exists := res[parameter]; exists { return nil, parse, fmt.Errorf("duplicate parameter %q at position %d", parameter, len(forwarded)-len(parse)) } res[parameter] = value parse = parse[len(value):] value = "" state = statePairEnd } // parse a part of quoted value until the first backslash case stateQuotedValue: match := reQuotedValue.FindString(parse) value += match parse = parse[len(match):] switch { case len(parse) == 0: return nil, parse, fmt.Errorf("unterminated quoted string") case parse[0] == '"': res[parameter] = value value = "" parse = parse[1:] state = statePairEnd case parse[0] == '\\': parse = parse[1:] state = stateEscapedCharacter } // parse escaped character in a quoted string, ignore the backslash // transition back to QuotedValue state case stateEscapedCharacter: c := reEscapedCharacter.FindString(parse) if len(c) == 0 { return nil, parse, fmt.Errorf("invalid escape sequence at position %d", len(forwarded)-len(parse)-1) } value += c parse = parse[1:] state = stateQuotedValue // expect either a new key-value pair, new list or end of input case statePairEnd: switch parse[0] { case ';': parse = parse[1:] state = stateParameter case ',': state = stateElement default: return nil, parse, fmt.Errorf("expected ',' or ';', not %c at position %d", parse[0], len(forwarded)-len(parse)) } } } return res, parse, nil } docker-registry-2.6.2~ds1/registry/api/v2/headerparser_test.go000066400000000000000000000074031313450123100244440ustar00rootroot00000000000000package v2 import ( "testing" ) func TestParseForwardedHeader(t *testing.T) { for _, tc := range []struct { name string raw string expected map[string]string expectedRest string expectedError bool }{ { name: "empty", raw: "", }, { name: "one pair", raw: " key = value ", expected: map[string]string{"key": "value"}, }, { name: "two pairs", raw: " key1 = value1; key2=value2", expected: map[string]string{"key1": "value1", "key2": "value2"}, }, { name: "uppercase parameter", raw: "KeY=VaL", expected: map[string]string{"key": "VaL"}, }, { name: "missing key=value pair - be tolerant", raw: "key=val;", expected: map[string]string{"key": "val"}, }, { name: "quoted values", raw: `key="val";param = "[[ $((1 + 1)) == 3 ]] && echo panic!;" ; p=" abcd "`, expected: map[string]string{"key": "val", "param": "[[ $((1 + 1)) == 3 ]] && echo panic!;", "p": " abcd "}, }, { name: "empty quoted value", raw: `key=""`, expected: map[string]string{"key": ""}, }, { name: "quoted double quotes", raw: `key="\"value\""`, expected: map[string]string{"key": `"value"`}, }, { name: "quoted backslash", raw: `key="\"\\\""`, expected: map[string]string{"key": `"\"`}, }, { name: "ignore subsequent elements", raw: "key=a, param= b", expected: map[string]string{"key": "a"}, expectedRest: " param= b", }, { name: "empty element - be tolerant", raw: " , key=val", expectedRest: " key=val", }, { name: "obscure key", raw: `ob₷C&r€ = value`, expected: map[string]string{`ob₷c&r€`: "value"}, }, { name: "duplicate parameter", raw: "key=a; p=b; key=c", expectedError: true, }, { name: "empty parameter", raw: "=value", expectedError: true, }, { name: "empty value", raw: "key= ", expectedError: true, }, { name: "empty value before a new element ", raw: "key=,", expectedError: true, }, { name: "empty value before a new pair", raw: "key=;", expectedError: true, }, { name: "just parameter", raw: "key", expectedError: true, }, { name: "missing key-value", raw: "a=b;;", expectedError: true, }, { name: "unclosed quoted value", raw: `key="value`, expectedError: true, }, { name: "escaped terminating dquote", raw: `key="value\"`, expectedError: true, }, { name: "just a quoted value", raw: `"key=val"`, expectedError: true, }, { name: "quoted key", raw: `"key"=val`, expectedError: true, }, } { parsed, rest, err := parseForwardedHeader(tc.raw) if err != nil && !tc.expectedError { t.Errorf("[%s] got unexpected error: %v", tc.name, err) } if err == nil && tc.expectedError { t.Errorf("[%s] got unexpected non-error", tc.name) } if err != nil || tc.expectedError { continue } for key, value := range tc.expected { v, exists := parsed[key] if !exists { t.Errorf("[%s] missing expected parameter %q", tc.name, key) continue } if v != value { t.Errorf("[%s] got unexpected value for parameter %q: %q != %q", tc.name, key, v, value) } } for key, value := range parsed { if _, exists := tc.expected[key]; !exists { t.Errorf("[%s] got unexpected key/value pair: %q=%q", tc.name, key, value) } } if rest != tc.expectedRest { t.Errorf("[%s] got unexpected unparsed string: %q != %q", tc.name, rest, tc.expectedRest) } } } docker-registry-2.6.2~ds1/registry/api/v2/routes.go000066400000000000000000000023621313450123100222600ustar00rootroot00000000000000package 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" RouteNameCatalog = "catalog" ) var allEndpoints = []string{ RouteNameManifest, RouteNameCatalog, 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 } docker-registry-2.6.2~ds1/registry/api/v2/routes_test.go000066400000000000000000000230531313450123100233170ustar00rootroot00000000000000package v2 import ( "encoding/json" "fmt" "math/rand" "net/http" "net/http/httptest" "reflect" "strings" "testing" "time" "github.com/gorilla/mux" ) type routeTestCase struct { RequestURI string ExpectedURI string Vars map[string]string RouteName string StatusCode int } // TestRouter registers a test handler with all the routes and ensures that // each route returns the expected path variables. Not method verification is // present. This not meant to be exhaustive but as check to ensure that the // expected variables are extracted. // // This may go away as the application structure comes together. func TestRouter(t *testing.T) { testCases := []routeTestCase{ { RouteName: RouteNameBase, RequestURI: "/v2/", Vars: map[string]string{}, }, { RouteName: RouteNameManifest, RequestURI: "/v2/foo/manifests/bar", Vars: map[string]string{ "name": "foo", "reference": "bar", }, }, { RouteName: RouteNameManifest, RequestURI: "/v2/foo/bar/manifests/tag", Vars: map[string]string{ "name": "foo/bar", "reference": "tag", }, }, { RouteName: RouteNameManifest, RequestURI: "/v2/foo/bar/manifests/sha256:abcdef01234567890", Vars: map[string]string{ "name": "foo/bar", "reference": "sha256:abcdef01234567890", }, }, { RouteName: RouteNameTags, RequestURI: "/v2/foo/bar/tags/list", Vars: map[string]string{ "name": "foo/bar", }, }, { RouteName: RouteNameTags, RequestURI: "/v2/docker.com/foo/tags/list", Vars: map[string]string{ "name": "docker.com/foo", }, }, { RouteName: RouteNameTags, RequestURI: "/v2/docker.com/foo/bar/tags/list", Vars: map[string]string{ "name": "docker.com/foo/bar", }, }, { RouteName: RouteNameTags, RequestURI: "/v2/docker.com/foo/bar/baz/tags/list", Vars: map[string]string{ "name": "docker.com/foo/bar/baz", }, }, { RouteName: RouteNameBlob, RequestURI: "/v2/foo/bar/blobs/sha256:abcdef0919234", Vars: map[string]string{ "name": "foo/bar", "digest": "sha256:abcdef0919234", }, }, { RouteName: RouteNameBlobUpload, RequestURI: "/v2/foo/bar/blobs/uploads/", Vars: map[string]string{ "name": "foo/bar", }, }, { RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/uuid", Vars: map[string]string{ "name": "foo/bar", "uuid": "uuid", }, }, { // support uuid proper RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", Vars: map[string]string{ "name": "foo/bar", "uuid": "D95306FA-FAD3-4E36-8D41-CF1C93EF8286", }, }, { RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", Vars: map[string]string{ "name": "foo/bar", "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA==", }, }, { // supports urlsafe base64 RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==", Vars: map[string]string{ "name": "foo/bar", "uuid": "RDk1MzA2RkEtRkFEMy00RTM2LThENDEtQ0YxQzkzRUY4Mjg2IA_-==", }, }, { // does not match RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/bar/blobs/uploads/totalandcompletejunk++$$-==", StatusCode: http.StatusNotFound, }, { // Check ambiguity: ensure we can distinguish between tags for // "foo/bar/image/image" and image for "foo/bar/image" with tag // "tags" RouteName: RouteNameManifest, RequestURI: "/v2/foo/bar/manifests/manifests/tags", Vars: map[string]string{ "name": "foo/bar/manifests", "reference": "tags", }, }, { // This case presents an ambiguity between foo/bar with tag="tags" // and list tags for "foo/bar/manifest" RouteName: RouteNameTags, RequestURI: "/v2/foo/bar/manifests/tags/list", Vars: map[string]string{ "name": "foo/bar/manifests", }, }, { RouteName: RouteNameManifest, RequestURI: "/v2/locahost:8080/foo/bar/baz/manifests/tag", Vars: map[string]string{ "name": "locahost:8080/foo/bar/baz", "reference": "tag", }, }, } checkTestRouter(t, testCases, "", true) checkTestRouter(t, testCases, "/prefix/", true) } func TestRouterWithPathTraversals(t *testing.T) { testCases := []routeTestCase{ { RouteName: RouteNameBlobUploadChunk, RequestURI: "/v2/foo/../../blob/uploads/D95306FA-FAD3-4E36-8D41-CF1C93EF8286", 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 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 resp, err := http.Get(u) if err != nil { t.Fatalf("error issuing get request: %v", err) } if testcase.StatusCode == 0 { // Override default, zero-value testcase.StatusCode = http.StatusOK } if testcase.ExpectedURI == "" { // Override default, zero-value testcase.ExpectedURI = testcase.RequestURI } if resp.StatusCode != testcase.StatusCode { t.Fatalf("unexpected status for %s: %v %v", u, resp.Status, resp.StatusCode) } if testcase.StatusCode != http.StatusOK { resp.Body.Close() // We don't care about json response. continue } dec := json.NewDecoder(resp.Body) var actualRouteInfo routeTestCase if err := dec.Decode(&actualRouteInfo); err != nil { t.Fatalf("error reading json response: %v", err) } // Needs to be set out of band 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 { t.Fatalf("incorrect route %q matched, expected %q", actualRouteInfo.RouteName, testcase.RouteName) } // 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) } resp.Body.Close() } } // -------------- 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 -------------- docker-registry-2.6.2~ds1/registry/api/v2/urls.go000066400000000000000000000157241313450123100217320ustar00rootroot00000000000000package v2 import ( "net/http" "net/url" "strings" "github.com/docker/distribution/reference" "github.com/gorilla/mux" ) // URLBuilder 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 URLBuilder struct { root *url.URL // url root (ie http://localhost/) router *mux.Router relative bool } // NewURLBuilder creates a URLBuilder with provided root url object. func NewURLBuilder(root *url.URL, relative bool) *URLBuilder { return &URLBuilder{ root: root, router: Router(), relative: relative, } } // NewURLBuilderFromString workes identically to NewURLBuilder except it takes // a string argument for the root, returning an error if it is not a valid // url. func NewURLBuilderFromString(root string, relative bool) (*URLBuilder, error) { u, err := url.Parse(root) if err != nil { return nil, err } return NewURLBuilder(u, relative), nil } // NewURLBuilderFromRequest uses information from an *http.Request to // construct the root url. func NewURLBuilderFromRequest(r *http.Request, relative bool) *URLBuilder { 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 := routeDescriptorsMap[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 NewURLBuilder(u, relative) } // BuildBaseURL constructs a base url for the API, typically just "/v2/". func (ub *URLBuilder) BuildBaseURL() (string, error) { route := ub.cloneRoute(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 *URLBuilder) BuildCatalogURL(values ...url.Values) (string, error) { route := ub.cloneRoute(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 *URLBuilder) BuildTagsURL(name reference.Named) (string, error) { route := ub.cloneRoute(RouteNameTags) tagsURL, err := route.URL("name", name.Name()) if err != nil { return "", err } return tagsURL.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 *URLBuilder) BuildManifestURL(ref reference.Named) (string, error) { route := ub.cloneRoute(RouteNameManifest) tagOrDigest := "" switch v := ref.(type) { case reference.Tagged: tagOrDigest = v.Tag() case reference.Digested: tagOrDigest = v.Digest().String() } 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 *URLBuilder) BuildBlobURL(ref reference.Canonical) (string, error) { route := ub.cloneRoute(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 *URLBuilder) BuildBlobUploadURL(name reference.Named, values ...url.Values) (string, error) { route := ub.cloneRoute(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 *URLBuilder) BuildBlobUploadChunkURL(name reference.Named, uuid string, values ...url.Values) (string, error) { route := ub.cloneRoute(RouteNameBlobUploadChunk) uploadURL, err := route.URL("name", name.Name(), "uuid", uuid) if err != nil { return "", err } return appendValuesURL(uploadURL, values...).String(), nil } // clondedRoute returns a clone of the named route from the router. Routes // must be cloned to avoid modifying them during url generation. func (ub *URLBuilder) cloneRoute(name string) clonedRoute { route := new(mux.Route) root := new(url.URL) *route = *ub.router.GetRoute(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:] } url := cr.root.ResolveReference(routeURL) url.Scheme = cr.root.Scheme return url, 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 } // appendValues appends the parameters to the url. Panics if the string is not // a url. func appendValues(u string, values ...url.Values) string { up, err := url.Parse(u) if err != nil { panic(err) // should never happen } return appendValuesURL(up, values...).String() } docker-registry-2.6.2~ds1/registry/api/v2/urls_test.go000066400000000000000000000346111313450123100227650ustar00rootroot00000000000000package v2 import ( "net/http" "net/url" "testing" "github.com/docker/distribution/reference" ) type urlBuilderTestCase struct { description string expectedPath string build func() (string, error) } func makeURLBuilderTestCases(urlBuilder *URLBuilder) []urlBuilderTestCase { fooBarRef, _ := reference.ParseNamed("foo/bar") 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(fooBarRef) }, }, { description: "test manifest url", expectedPath: "/v2/foo/bar/manifests/tag", build: func() (string, error) { ref, _ := reference.WithTag(fooBarRef, "tag") return urlBuilder.BuildManifestURL(ref) }, }, { description: "build blob url", expectedPath: "/v2/foo/bar/blobs/sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5", build: func() (string, error) { ref, _ := reference.WithDigest(fooBarRef, "sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5") return urlBuilder.BuildBlobURL(ref) }, }, { description: "build blob upload url", expectedPath: "/v2/foo/bar/blobs/uploads/", build: func() (string, error) { return urlBuilder.BuildBlobUploadURL(fooBarRef) }, }, { description: "build blob upload url with digest and size", expectedPath: "/v2/foo/bar/blobs/uploads/?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000", build: func() (string, error) { return urlBuilder.BuildBlobUploadURL(fooBarRef, url.Values{ "size": []string{"10000"}, "digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"}, }) }, }, { description: "build blob upload chunk url", expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part", build: func() (string, error) { return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part") }, }, { description: "build blob upload chunk url with digest and size", expectedPath: "/v2/foo/bar/blobs/uploads/uuid-part?digest=sha256%3A3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5&size=10000", build: func() (string, error) { return urlBuilder.BuildBlobUploadChunkURL(fooBarRef, "uuid-part", url.Values{ "size": []string{"10000"}, "digest": []string{"sha256:3b3692957d439ac1928219a83fac91e7bf96c153725526874673ae1f2023f8d5"}, }) }, }, } } // 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", } doTest := func(relative bool) { for _, root := range roots { urlBuilder, err := NewURLBuilderFromString(root, relative) 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 := testCase.expectedPath if !relative { expectedURL = root + expectedURL } if url != expectedURL { t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL) } } } } doTest(true) doTest(false) } func TestURLBuilderWithPrefix(t *testing.T) { roots := []string{ "http://example.com/prefix/", "https://example.com/prefix/", "http://localhost:5000/prefix/", "https://localhost:5443/prefix/", } doTest := func(relative bool) { for _, root := range roots { urlBuilder, err := NewURLBuilderFromString(root, relative) 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 := testCase.expectedPath if !relative { expectedURL = root[0:len(root)-1] + expectedURL } if url != expectedURL { t.Fatalf("%s: %q != %q", testCase.description, url, expectedURL) } } } } doTest(true) doTest(false) } 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) } testRequests := []struct { name string request *http.Request base string configHost url.URL }{ { name: "no forwarded header", request: &http.Request{URL: u, Host: u.Host}, base: "http://example.com", }, { name: "https protocol forwarded with a non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Custom-Forwarded-Proto": []string{"https"}, }}, base: "http://example.com", }, { name: "forwarded protocol is the same", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{"https"}, }}, base: "https://example.com", }, { name: "forwarded host with a non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com"}, }}, base: "http://first.example.com", }, { name: "forwarded multiple hosts a with non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"}, }}, base: "http://first.example.com", }, { name: "host configured in config file takes priority", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com, proxy1.example.com"}, }}, base: "https://third.example.com:5000", configHost: url.URL{ Scheme: "https", Host: "third.example.com:5000", }, }, { name: "forwarded host and port with just one non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com:443"}, }}, base: "http://first.example.com:443", }, { name: "forwarded port with a non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"example.com:5000"}, "X-Forwarded-Port": []string{"5000"}, }}, base: "http://example.com:5000", }, { name: "forwarded multiple ports with a non-standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Port": []string{"443 , 5001"}, }}, base: "http://example.com", }, { name: "forwarded standard port with non-standard headers", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{"example.com"}, "X-Forwarded-Port": []string{"443"}, }}, base: "https://example.com", }, { name: "forwarded standard port with non-standard headers and explicit port", request: &http.Request{URL: u, Host: u.Host + ":443", Header: http.Header{ "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{u.Host + ":443"}, "X-Forwarded-Port": []string{"443"}, }}, base: "https://example.com:443", }, { name: "several non-standard headers", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{"https"}, "X-Forwarded-Host": []string{" first.example.com:12345 "}, }}, base: "https://first.example.com:12345", }, { name: "forwarded host with port supplied takes priority", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com:5000"}, "X-Forwarded-Port": []string{"80"}, }}, base: "http://first.example.com:5000", }, { name: "malformed forwarded port", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Host": []string{"first.example.com"}, "X-Forwarded-Port": []string{"abcd"}, }}, base: "http://first.example.com", }, { name: "forwarded protocol and addr using standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`proto=https;host="192.168.22.30:80"`}, }}, base: "https://192.168.22.30:80", }, { name: "forwarded host takes priority over for", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`host="reg.example.com:5000";for="192.168.22.30"`}, }}, base: "http://reg.example.com:5000", }, { name: "forwarded host and protocol using standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`host=reg.example.com;proto=https`}, }}, base: "https://reg.example.com", }, { name: "process just the first standard forwarded header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`host="reg.example.com:88";proto=http`, `host=reg.example.com;proto=https`}, }}, base: "http://reg.example.com:88", }, { name: "process just the first list element of standard header", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`host="reg.example.com:443";proto=https, host="reg.example.com:80";proto=http`}, }}, base: "https://reg.example.com:443", }, { name: "IPv6 address use host", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`for="2607:f0d0:1002:51::4";host="[2607:f0d0:1002:51::4]:5001"`}, "X-Forwarded-Port": []string{"5002"}, }}, base: "http://[2607:f0d0:1002:51::4]:5001", }, { name: "IPv6 address with port", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "Forwarded": []string{`host="[2607:f0d0:1002:51::4]:4000"`}, "X-Forwarded-Port": []string{"5001"}, }}, base: "http://[2607:f0d0:1002:51::4]:4000", }, { name: "non-standard and standard forward headers", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{`https`}, "X-Forwarded-Host": []string{`first.example.com`}, "X-Forwarded-Port": []string{``}, "Forwarded": []string{`host=first.example.com; proto=https`}, }}, base: "https://first.example.com", }, { name: "standard header takes precedence over non-standard headers", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{`http`}, "Forwarded": []string{`host=second.example.com; proto=https`}, "X-Forwarded-Host": []string{`first.example.com`}, "X-Forwarded-Port": []string{`4000`}, }}, base: "https://second.example.com", }, { name: "incomplete standard header uses default", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{`https`}, "Forwarded": []string{`for=127.0.0.1`}, "X-Forwarded-Host": []string{`first.example.com`}, "X-Forwarded-Port": []string{`4000`}, }}, base: "http://" + u.Host, }, { name: "standard with just proto", request: &http.Request{URL: u, Host: u.Host, Header: http.Header{ "X-Forwarded-Proto": []string{`https`}, "Forwarded": []string{`proto=https`}, "X-Forwarded-Host": []string{`first.example.com`}, "X-Forwarded-Port": []string{`4000`}, }}, base: "https://" + u.Host, }, } doTest := func(relative bool) { for _, tr := range testRequests { var builder *URLBuilder if tr.configHost.Scheme != "" && tr.configHost.Host != "" { builder = NewURLBuilder(&tr.configHost, relative) } else { builder = NewURLBuilderFromRequest(tr.request, relative) } for _, testCase := range makeURLBuilderTestCases(builder) { buildURL, err := testCase.build() if err != nil { t.Fatalf("[relative=%t, request=%q, case=%q]: error building url: %v", relative, tr.name, testCase.description, err) } expectedURL := testCase.expectedPath if !relative { expectedURL = tr.base + expectedURL } if buildURL != expectedURL { t.Errorf("[relative=%t, request=%q, case=%q]: %q != %q", relative, tr.name, testCase.description, buildURL, expectedURL) } } } } doTest(true) doTest(false) } 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 configHost url.URL }{ { request: &http.Request{URL: u, Host: u.Host}, base: "http://example.com/prefix/", }, { request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, base: "http://example.com/prefix/", }, { request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, base: "https://example.com/prefix/", }, { request: &http.Request{URL: u, Host: u.Host, Header: forwardedProtoHeader}, base: "https://subdomain.example.com/prefix/", configHost: url.URL{ Scheme: "https", Host: "subdomain.example.com", Path: "/prefix/", }, }, } var relative bool for _, tr := range testRequests { var builder *URLBuilder if tr.configHost.Scheme != "" && tr.configHost.Host != "" { builder = NewURLBuilder(&tr.configHost, false) } else { builder = NewURLBuilderFromRequest(tr.request, false) } for _, testCase := range makeURLBuilderTestCases(builder) { buildURL, err := testCase.build() if err != nil { t.Fatalf("%s: error building url: %v", testCase.description, err) } var expectedURL string proto, ok := tr.request.Header["X-Forwarded-Proto"] if !ok { expectedURL = testCase.expectedPath if !relative { expectedURL = tr.base[0:len(tr.base)-1] + expectedURL } } else { urlBase, err := url.Parse(tr.base) if err != nil { t.Fatal(err) } urlBase.Scheme = proto[0] expectedURL = testCase.expectedPath if !relative { expectedURL = urlBase.String()[0:len(urlBase.String())-1] + expectedURL } } if buildURL != expectedURL { t.Fatalf("%s: %q != %q", testCase.description, buildURL, expectedURL) } } } } docker-registry-2.6.2~ds1/registry/auth/000077500000000000000000000000001313450123100202465ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/auth/auth.go000066400000000000000000000140151313450123100215370ustar00rootroot00000000000000// Package auth defines a standard interface for request access controllers. // // An access controller has a simple interface with a single `Authorized` // method which checks that a given request is authorized to perform one or // more actions on one or more resources. This method should return a non-nil // error if the request is not authorized. // // An implementation registers its access controller by name with a constructor // which accepts an options map for configuring the access controller. // // options := map[string]interface{}{"sillySecret": "whysosilly?"} // accessController, _ := auth.GetAccessController("silly", options) // // This `accessController` can then be used in a request handler like so: // // func updateOrder(w http.ResponseWriter, r *http.Request) { // orderNumber := r.FormValue("orderNumber") // resource := auth.Resource{Type: "customerOrder", Name: orderNumber} // access := auth.Access{Resource: resource, Action: "update"} // // if ctx, err := accessController.Authorized(ctx, access); err != nil { // if challenge, ok := err.(auth.Challenge) { // // Let the challenge write the response. // challenge.SetHeaders(w) // w.WriteHeader(http.StatusUnauthorized) // return // } else { // // Some other error. // } // } // } // package auth import ( "errors" "fmt" "net/http" "github.com/docker/distribution/context" ) const ( // UserKey is used to get the user object from // a user context UserKey = "auth.user" // UserNameKey is used to get the user name from // a user context UserNameKey = "auth.user.name" ) var ( // ErrInvalidCredential is returned when the auth token does not authenticate correctly. ErrInvalidCredential = errors.New("invalid authorization credential") // ErrAuthenticationFailure returned when authentication fails. ErrAuthenticationFailure = errors.New("authentication failure") ) // UserInfo carries information about // an autenticated/authorized client. type UserInfo struct { Name string } // Resource describes a resource by type and name. type Resource struct { Type string Class string Name string } // Access describes a specific action that is // requested or allowed for a given resource. type Access struct { Resource Action string } // Challenge is a special error type which is used for HTTP 401 Unauthorized // responses and is able to write the response with WWW-Authenticate challenge // header values based on the error. type Challenge interface { error // SetHeaders prepares the request to conduct a challenge response by // adding the an HTTP challenge header on the response message. Callers // are expected to set the appropriate HTTP status code (e.g. 401) // themselves. SetHeaders(w http.ResponseWriter) } // AccessController controls access to registry resources based on a request // and required access levels for a request. Implementations can support both // complete denial and http authorization challenges. type AccessController interface { // Authorized returns a non-nil error if the context is granted access and // returns a new authorized context. If one or more Access structs are // provided, the requested access will be compared with what is available // to the context. The given context will contain a "http.request" key with // a `*http.Request` value. If the error is non-nil, access should always // be denied. The error may be of type Challenge, in which case the caller // may have the Challenge handle the request or choose what action to take // based on the Challenge header or response status. The returned context // object should have a "auth.user" value set to a UserInfo struct. Authorized(ctx context.Context, access ...Access) (context.Context, error) } // CredentialAuthenticator is an object which is able to authenticate credentials type CredentialAuthenticator interface { AuthenticateUser(username, password string) error } // WithUser returns a context with the authorized user info. func WithUser(ctx context.Context, user UserInfo) context.Context { return userInfoContext{ Context: ctx, user: user, } } type userInfoContext struct { context.Context user UserInfo } func (uic userInfoContext) Value(key interface{}) interface{} { switch key { case UserKey: return uic.user case UserNameKey: return uic.user.Name } return uic.Context.Value(key) } // WithResources returns a context with the authorized resources. func WithResources(ctx context.Context, resources []Resource) context.Context { return resourceContext{ Context: ctx, resources: resources, } } type resourceContext struct { context.Context resources []Resource } type resourceKey struct{} func (rc resourceContext) Value(key interface{}) interface{} { if key == (resourceKey{}) { return rc.resources } return rc.Context.Value(key) } // AuthorizedResources returns the list of resources which have // been authorized for this request. func AuthorizedResources(ctx context.Context) []Resource { if resources, ok := ctx.Value(resourceKey{}).([]Resource); ok { return resources } return nil } // InitFunc is the type of an AccessController factory function and is used // to register the constructor for different AccesController backends. type InitFunc func(options map[string]interface{}) (AccessController, error) var accessControllers map[string]InitFunc func init() { accessControllers = make(map[string]InitFunc) } // Register is used to register an InitFunc for // an AccessController backend with the given name. func Register(name string, initFunc InitFunc) error { if _, exists := accessControllers[name]; exists { return fmt.Errorf("name already registered: %s", name) } accessControllers[name] = initFunc return nil } // GetAccessController constructs an AccessController // with the given options using the named backend. func GetAccessController(name string, options map[string]interface{}) (AccessController, error) { if initFunc, exists := accessControllers[name]; exists { return initFunc(options) } return nil, fmt.Errorf("no access controller registered with name: %s", name) } docker-registry-2.6.2~ds1/registry/auth/htpasswd/000077500000000000000000000000001313450123100221035ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/auth/htpasswd/access.go000066400000000000000000000054271313450123100237030ustar00rootroot00000000000000// Package htpasswd provides a simple authentication scheme that checks for the // user credential hash in an htpasswd formatted file in a configuration-determined // location. // // This authentication method MUST be used under TLS, as simple token-replay attack is possible. package htpasswd import ( "fmt" "net/http" "os" "sync" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" ) type accessController struct { realm string path string modtime time.Time mu sync.Mutex htpasswd *htpasswd } var _ auth.AccessController = &accessController{} func newAccessController(options map[string]interface{}) (auth.AccessController, error) { realm, present := options["realm"] if _, ok := realm.(string); !present || !ok { return nil, fmt.Errorf(`"realm" must be set for htpasswd access controller`) } path, present := options["path"] if _, ok := path.(string); !present || !ok { return nil, fmt.Errorf(`"path" must be set for htpasswd access controller`) } return &accessController{realm: realm.(string), path: path.(string)}, nil } func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { req, err := context.GetRequest(ctx) if err != nil { return nil, err } username, password, ok := req.BasicAuth() if !ok { return nil, &challenge{ realm: ac.realm, err: auth.ErrInvalidCredential, } } // Dynamically parsing the latest account list fstat, err := os.Stat(ac.path) if err != nil { return nil, err } lastModified := fstat.ModTime() ac.mu.Lock() if ac.htpasswd == nil || !ac.modtime.Equal(lastModified) { ac.modtime = lastModified f, err := os.Open(ac.path) if err != nil { ac.mu.Unlock() return nil, err } defer f.Close() h, err := newHTPasswd(f) if err != nil { ac.mu.Unlock() return nil, err } ac.htpasswd = h } localHTPasswd := ac.htpasswd ac.mu.Unlock() if err := localHTPasswd.authenticateUser(username, password); err != nil { context.GetLogger(ctx).Errorf("error authenticating user %q: %v", username, err) return nil, &challenge{ realm: ac.realm, err: auth.ErrAuthenticationFailure, } } return auth.WithUser(ctx, auth.UserInfo{Name: username}), nil } // challenge implements the auth.Challenge interface. type challenge struct { realm string err error } var _ auth.Challenge = challenge{} // SetHeaders sets the basic challenge header on the response. func (ch challenge) SetHeaders(w http.ResponseWriter) { w.Header().Set("WWW-Authenticate", fmt.Sprintf("Basic realm=%q", ch.realm)) } func (ch challenge) Error() string { return fmt.Sprintf("basic authentication challenge for realm %q: %s", ch.realm, ch.err) } func init() { auth.Register("htpasswd", auth.InitFunc(newAccessController)) } docker-registry-2.6.2~ds1/registry/auth/htpasswd/access_test.go000066400000000000000000000064011313450123100247330ustar00rootroot00000000000000package htpasswd import ( "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" ) func TestBasicAccessController(t *testing.T) { testRealm := "The-Shire" testUsers := []string{"bilbo", "frodo", "MiShil", "DeokMan"} testPasswords := []string{"baggins", "baggins", "새주", "공주님"} testHtpasswdContent := `bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 DeokMan:공주님` tempFile, err := ioutil.TempFile("", "htpasswd-test") if err != nil { t.Fatal("could not create temporary htpasswd file") } if _, err = tempFile.WriteString(testHtpasswdContent); err != nil { t.Fatal("could not write temporary htpasswd file") } options := map[string]interface{}{ "realm": testRealm, "path": tempFile.Name(), } ctx := context.Background() accessController, err := newAccessController(options) if err != nil { t.Fatal("error creating access controller") } tempFile.Close() var userNumber = 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithRequest(ctx, r) authCtx, err := accessController.Authorized(ctx) if err != nil { switch err := err.(type) { case auth.Challenge: err.SetHeaders(w) w.WriteHeader(http.StatusUnauthorized) return default: t.Fatalf("unexpected error authorizing request: %v", err) } } userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if !ok { t.Fatal("basic accessController did not set auth.user context") } if userInfo.Name != testUsers[userNumber] { t.Fatalf("expected user name %q, got %q", testUsers[userNumber], userInfo.Name) } w.WriteHeader(http.StatusNoContent) })) client := &http.Client{ CheckRedirect: nil, } req, _ := http.NewRequest("GET", server.URL, nil) resp, err := client.Do(req) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() // Request should not be authorized if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unexpected non-fail response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } nonbcrypt := map[string]struct{}{ "bilbo": {}, "DeokMan": {}, } for i := 0; i < len(testUsers); i++ { userNumber = i req, err := http.NewRequest("GET", server.URL, nil) if err != nil { t.Fatalf("error allocating new request: %v", err) } req.SetBasicAuth(testUsers[i], testPasswords[i]) resp, err = client.Do(req) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() if _, ok := nonbcrypt[testUsers[i]]; ok { // these are not allowed. // Request should be authorized if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusUnauthorized, testUsers[i], testPasswords[i]) } } else { // Request should be authorized if resp.StatusCode != http.StatusNoContent { t.Fatalf("unexpected non-success response status: %v != %v for %s %s", resp.StatusCode, http.StatusNoContent, testUsers[i], testPasswords[i]) } } } } docker-registry-2.6.2~ds1/registry/auth/htpasswd/htpasswd.go000066400000000000000000000037521313450123100242760ustar00rootroot00000000000000package htpasswd import ( "bufio" "fmt" "io" "strings" "github.com/docker/distribution/registry/auth" "golang.org/x/crypto/bcrypt" ) // htpasswd holds a path to a system .htpasswd file and the machinery to parse // it. Only bcrypt hash entries are supported. type htpasswd struct { entries map[string][]byte // maps username to password byte slice. } // newHTPasswd parses the reader and returns an htpasswd or an error. func newHTPasswd(rd io.Reader) (*htpasswd, error) { entries, err := parseHTPasswd(rd) if err != nil { return nil, err } return &htpasswd{entries: entries}, nil } // AuthenticateUser checks a given user:password credential against the // receiving HTPasswd's file. If the check passes, nil is returned. func (htpasswd *htpasswd) authenticateUser(username string, password string) error { credentials, ok := htpasswd.entries[username] if !ok { // timing attack paranoia bcrypt.CompareHashAndPassword([]byte{}, []byte(password)) return auth.ErrAuthenticationFailure } err := bcrypt.CompareHashAndPassword([]byte(credentials), []byte(password)) if err != nil { return auth.ErrAuthenticationFailure } return nil } // parseHTPasswd parses the contents of htpasswd. This will read all the // entries in the file, whether or not they are needed. An error is returned // if a syntax errors are encountered or if the reader fails. func parseHTPasswd(rd io.Reader) (map[string][]byte, error) { entries := map[string][]byte{} scanner := bufio.NewScanner(rd) var line int for scanner.Scan() { line++ // 1-based line numbering t := strings.TrimSpace(scanner.Text()) if len(t) < 1 { continue } // lines that *begin* with a '#' are considered comments if t[0] == '#' { continue } i := strings.Index(t, ":") if i < 0 || i >= len(t) { return nil, fmt.Errorf("htpasswd: invalid entry at line %d: %q", line, scanner.Text()) } entries[t[:i]] = []byte(t[i+1:]) } if err := scanner.Err(); err != nil { return nil, err } return entries, nil } docker-registry-2.6.2~ds1/registry/auth/htpasswd/htpasswd_test.go000066400000000000000000000035031313450123100253270ustar00rootroot00000000000000package htpasswd import ( "fmt" "reflect" "strings" "testing" ) func TestParseHTPasswd(t *testing.T) { for _, tc := range []struct { desc string input string err error entries map[string][]byte }{ { desc: "basic example", input: ` # This is a comment in a basic example. bilbo:{SHA}5siv5c0SHx681xU6GiSx9ZQryqs= frodo:$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W MiShil:$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2 DeokMan:공주님 `, entries: map[string][]byte{ "bilbo": []byte("{SHA}5siv5c0SHx681xU6GiSx9ZQryqs="), "frodo": []byte("$2y$05$926C3y10Quzn/LnqQH86VOEVh/18T6RnLaS.khre96jLNL/7e.K5W"), "MiShil": []byte("$2y$05$0oHgwMehvoe8iAWS8I.7l.KoECXrwVaC16RPfaSCU5eVTFrATuMI2"), "DeokMan": []byte("공주님"), }, }, { desc: "ensures comments are filtered", input: ` # asdf:asdf `, }, { desc: "ensure midline hash is not comment", input: ` asdf:as#df `, entries: map[string][]byte{ "asdf": []byte("as#df"), }, }, { desc: "ensure midline hash is not comment", input: ` # A valid comment valid:entry asdf `, err: fmt.Errorf(`htpasswd: invalid entry at line 4: "asdf"`), }, } { entries, err := parseHTPasswd(strings.NewReader(tc.input)) if err != tc.err { if tc.err == nil { t.Fatalf("%s: unexpected error: %v", tc.desc, err) } else { if err.Error() != tc.err.Error() { // use string equality here. t.Fatalf("%s: expected error not returned: %v != %v", tc.desc, err, tc.err) } } } if tc.err != nil { continue // don't test output } // allow empty and nil to be equal if tc.entries == nil { tc.entries = map[string][]byte{} } if !reflect.DeepEqual(entries, tc.entries) { t.Fatalf("%s: entries not parsed correctly: %v != %v", tc.desc, entries, tc.entries) } } } docker-registry-2.6.2~ds1/registry/auth/silly/000077500000000000000000000000001313450123100214025ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/auth/silly/access.go000066400000000000000000000052771313450123100232050ustar00rootroot00000000000000// Package silly provides a simple authentication scheme that checks for the // existence of an Authorization header and issues access if is present and // non-empty. // // This package is present as an example implementation of a minimal // auth.AccessController and for testing. This is not suitable for any kind of // production security. package silly import ( "fmt" "net/http" "strings" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" ) // accessController provides a simple implementation of auth.AccessController // that simply checks for a non-empty Authorization header. It is useful for // demonstration and testing. type accessController struct { realm string service string } var _ auth.AccessController = &accessController{} func newAccessController(options map[string]interface{}) (auth.AccessController, error) { realm, present := options["realm"] if _, ok := realm.(string); !present || !ok { return nil, fmt.Errorf(`"realm" must be set for silly access controller`) } service, present := options["service"] if _, ok := service.(string); !present || !ok { return nil, fmt.Errorf(`"service" must be set for silly access controller`) } return &accessController{realm: realm.(string), service: service.(string)}, nil } // Authorized simply checks for the existence of the authorization header, // responding with a bearer challenge if it doesn't exist. func (ac *accessController) Authorized(ctx context.Context, accessRecords ...auth.Access) (context.Context, error) { req, err := context.GetRequest(ctx) if err != nil { return nil, err } if req.Header.Get("Authorization") == "" { challenge := challenge{ realm: ac.realm, service: ac.service, } if len(accessRecords) > 0 { var scopes []string for _, access := range accessRecords { scopes = append(scopes, fmt.Sprintf("%s:%s:%s", access.Type, access.Resource.Name, access.Action)) } challenge.scope = strings.Join(scopes, " ") } return nil, &challenge } return auth.WithUser(ctx, auth.UserInfo{Name: "silly"}), nil } type challenge struct { realm string service string scope string } var _ auth.Challenge = challenge{} // SetHeaders sets a simple bearer challenge on the response. func (ch challenge) SetHeaders(w http.ResponseWriter) { header := fmt.Sprintf("Bearer realm=%q,service=%q", ch.realm, ch.service) if ch.scope != "" { header = fmt.Sprintf("%s,scope=%q", header, ch.scope) } w.Header().Set("WWW-Authenticate", header) } func (ch challenge) Error() string { return fmt.Sprintf("silly authentication challenge: %#v", ch) } // init registers the silly auth backend. func init() { auth.Register("silly", auth.InitFunc(newAccessController)) } docker-registry-2.6.2~ds1/registry/auth/silly/access_test.go000066400000000000000000000034401313450123100242320ustar00rootroot00000000000000package silly import ( "net/http" "net/http/httptest" "testing" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" ) func TestSillyAccessController(t *testing.T) { ac := &accessController{ realm: "test-realm", service: "test-service", } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := context.WithRequest(context.Background(), r) authCtx, err := ac.Authorized(ctx) if err != nil { switch err := err.(type) { case auth.Challenge: err.SetHeaders(w) w.WriteHeader(http.StatusUnauthorized) return default: t.Fatalf("unexpected error authorizing request: %v", err) } } userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if !ok { t.Fatal("silly accessController did not set auth.user context") } if userInfo.Name != "silly" { t.Fatalf("expected user name %q, got %q", "silly", userInfo.Name) } w.WriteHeader(http.StatusNoContent) })) resp, err := http.Get(server.URL) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() // Request should not be authorized if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusUnauthorized) } req, err := http.NewRequest("GET", server.URL, nil) if err != nil { t.Fatalf("unexpected error creating new request: %v", err) } req.Header.Set("Authorization", "seriously, anything") resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer resp.Body.Close() // Request should not be authorized if resp.StatusCode != http.StatusNoContent { t.Fatalf("unexpected response status: %v != %v", resp.StatusCode, http.StatusNoContent) } } docker-registry-2.6.2~ds1/registry/auth/token/000077500000000000000000000000001313450123100213665ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/auth/token/accesscontroller.go000066400000000000000000000163041313450123100252660ustar00rootroot00000000000000package token import ( "crypto" "crypto/x509" "encoding/pem" "errors" "fmt" "io/ioutil" "net/http" "os" "strings" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" "github.com/docker/libtrust" ) // accessSet maps a typed, named resource to // a set of actions requested or authorized. type accessSet map[auth.Resource]actionSet // newAccessSet constructs an accessSet from // a variable number of auth.Access items. func newAccessSet(accessItems ...auth.Access) accessSet { accessSet := make(accessSet, len(accessItems)) for _, access := range accessItems { resource := auth.Resource{ Type: access.Type, Name: access.Name, } set, exists := accessSet[resource] if !exists { set = newActionSet() accessSet[resource] = set } set.add(access.Action) } return accessSet } // contains returns whether or not the given access is in this accessSet. func (s accessSet) contains(access auth.Access) bool { actionSet, ok := s[access.Resource] if ok { return actionSet.contains(access.Action) } return false } // scopeParam returns a collection of scopes which can // be used for a WWW-Authenticate challenge parameter. // See https://tools.ietf.org/html/rfc6750#section-3 func (s accessSet) scopeParam() string { scopes := make([]string, 0, len(s)) for resource, actionSet := range s { actions := strings.Join(actionSet.keys(), ",") scopes = append(scopes, fmt.Sprintf("%s:%s:%s", resource.Type, resource.Name, actions)) } return strings.Join(scopes, " ") } // Errors used and exported by this package. var ( ErrInsufficientScope = errors.New("insufficient scope") ErrTokenRequired = errors.New("authorization token required") ) // authChallenge implements the auth.Challenge interface. type authChallenge struct { err error realm string service string accessSet accessSet } var _ auth.Challenge = authChallenge{} // Error returns the internal error string for this authChallenge. func (ac authChallenge) Error() string { return ac.err.Error() } // Status returns the HTTP Response Status Code for this authChallenge. func (ac authChallenge) Status() int { return http.StatusUnauthorized } // challengeParams constructs the value to be used in // the WWW-Authenticate response challenge header. // See https://tools.ietf.org/html/rfc6750#section-3 func (ac authChallenge) challengeParams() string { str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service) if scope := ac.accessSet.scopeParam(); scope != "" { str = fmt.Sprintf("%s,scope=%q", str, scope) } if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken { str = fmt.Sprintf("%s,error=%q", str, "invalid_token") } else if ac.err == ErrInsufficientScope { str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope") } return str } // SetChallenge sets the WWW-Authenticate value for the response. func (ac authChallenge) SetHeaders(w http.ResponseWriter) { w.Header().Add("WWW-Authenticate", ac.challengeParams()) } // accessController implements the auth.AccessController interface. type accessController struct { realm string issuer string service string rootCerts *x509.CertPool trustedKeys map[string]libtrust.PublicKey } // tokenAccessOptions is a convenience type for handling // options to the contstructor of an accessController. type tokenAccessOptions struct { realm string issuer string service string rootCertBundle string } // checkOptions gathers the necessary options // for an accessController from the given map. func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) { var opts tokenAccessOptions keys := []string{"realm", "issuer", "service", "rootcertbundle"} vals := make([]string, 0, len(keys)) for _, key := range keys { val, ok := options[key].(string) if !ok { return opts, fmt.Errorf("token auth requires a valid option string: %q", key) } vals = append(vals, val) } opts.realm, opts.issuer, opts.service, opts.rootCertBundle = vals[0], vals[1], vals[2], vals[3] return opts, nil } // newAccessController creates an accessController using the given options. func newAccessController(options map[string]interface{}) (auth.AccessController, error) { config, err := checkOptions(options) if err != nil { return nil, err } fp, err := os.Open(config.rootCertBundle) if err != nil { return nil, fmt.Errorf("unable to open token auth root certificate bundle file %q: %s", config.rootCertBundle, err) } defer fp.Close() rawCertBundle, err := ioutil.ReadAll(fp) if err != nil { return nil, fmt.Errorf("unable to read token auth root certificate bundle file %q: %s", config.rootCertBundle, err) } var rootCerts []*x509.Certificate pemBlock, rawCertBundle := pem.Decode(rawCertBundle) for pemBlock != nil { if pemBlock.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { return nil, fmt.Errorf("unable to parse token auth root certificate: %s", err) } rootCerts = append(rootCerts, cert) } pemBlock, rawCertBundle = pem.Decode(rawCertBundle) } if len(rootCerts) == 0 { return nil, errors.New("token auth requires at least one token signing root certificate") } rootPool := x509.NewCertPool() trustedKeys := make(map[string]libtrust.PublicKey, len(rootCerts)) for _, rootCert := range rootCerts { rootPool.AddCert(rootCert) pubKey, err := libtrust.FromCryptoPublicKey(crypto.PublicKey(rootCert.PublicKey)) if err != nil { return nil, fmt.Errorf("unable to get public key from token auth root certificate: %s", err) } trustedKeys[pubKey.KeyID()] = pubKey } return &accessController{ realm: config.realm, issuer: config.issuer, service: config.service, rootCerts: rootPool, trustedKeys: trustedKeys, }, nil } // Authorized handles checking whether the given request is authorized // for actions on resources described by the given access items. func (ac *accessController) Authorized(ctx context.Context, accessItems ...auth.Access) (context.Context, error) { challenge := &authChallenge{ realm: ac.realm, service: ac.service, accessSet: newAccessSet(accessItems...), } req, err := context.GetRequest(ctx) if err != nil { return nil, err } parts := strings.Split(req.Header.Get("Authorization"), " ") if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" { challenge.err = ErrTokenRequired return nil, challenge } rawToken := parts[1] token, err := NewToken(rawToken) if err != nil { challenge.err = err return nil, challenge } verifyOpts := VerifyOptions{ TrustedIssuers: []string{ac.issuer}, AcceptedAudiences: []string{ac.service}, Roots: ac.rootCerts, TrustedKeys: ac.trustedKeys, } if err = token.Verify(verifyOpts); err != nil { challenge.err = err return nil, challenge } accessSet := token.accessSet() for _, access := range accessItems { if !accessSet.contains(access) { challenge.err = ErrInsufficientScope return nil, challenge } } ctx = auth.WithResources(ctx, token.resources()) return auth.WithUser(ctx, auth.UserInfo{Name: token.Claims.Subject}), nil } // init handles registering the token auth backend. func init() { auth.Register("token", auth.InitFunc(newAccessController)) } docker-registry-2.6.2~ds1/registry/auth/token/stringset.go000066400000000000000000000014051313450123100237370ustar00rootroot00000000000000package token // StringSet is a useful type for looking up strings. type stringSet map[string]struct{} // NewStringSet creates a new StringSet with the given strings. func newStringSet(keys ...string) stringSet { ss := make(stringSet, len(keys)) ss.add(keys...) return ss } // Add inserts the given keys into this StringSet. func (ss stringSet) add(keys ...string) { for _, key := range keys { ss[key] = struct{}{} } } // Contains returns whether the given key is in this StringSet. func (ss stringSet) contains(key string) bool { _, ok := ss[key] return ok } // Keys returns a slice of all keys in this StringSet. func (ss stringSet) keys() []string { keys := make([]string, 0, len(ss)) for key := range ss { keys = append(keys, key) } return keys } docker-registry-2.6.2~ds1/registry/auth/token/token.go000066400000000000000000000255251313450123100230460ustar00rootroot00000000000000package token import ( "crypto" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/docker/libtrust" "github.com/docker/distribution/registry/auth" ) const ( // TokenSeparator is the value which separates the header, claims, and // signature in the compact serialization of a JSON Web Token. TokenSeparator = "." // Leeway is the Duration that will be added to NBF and EXP claim // checks to account for clock skew as per https://tools.ietf.org/html/rfc7519#section-4.1.5 Leeway = 60 * time.Second ) // Errors used by token parsing and verification. var ( ErrMalformedToken = errors.New("malformed token") ErrInvalidToken = errors.New("invalid token") ) // ResourceActions stores allowed actions on a named and typed resource. type ResourceActions struct { Type string `json:"type"` Class string `json:"class,omitempty"` Name string `json:"name"` Actions []string `json:"actions"` } // ClaimSet describes the main section of a JSON Web Token. type ClaimSet struct { // Public claims Issuer string `json:"iss"` Subject string `json:"sub"` Audience string `json:"aud"` Expiration int64 `json:"exp"` NotBefore int64 `json:"nbf"` IssuedAt int64 `json:"iat"` JWTID string `json:"jti"` // Private claims Access []*ResourceActions `json:"access"` } // Header describes the header section of a JSON Web Token. type Header struct { Type string `json:"typ"` SigningAlg string `json:"alg"` KeyID string `json:"kid,omitempty"` X5c []string `json:"x5c,omitempty"` RawJWK *json.RawMessage `json:"jwk,omitempty"` } // Token describes a JSON Web Token. type Token struct { Raw string Header *Header Claims *ClaimSet Signature []byte } // VerifyOptions is used to specify // options when verifying a JSON Web Token. type VerifyOptions struct { TrustedIssuers []string AcceptedAudiences []string Roots *x509.CertPool TrustedKeys map[string]libtrust.PublicKey } // NewToken parses the given raw token string // and constructs an unverified JSON Web Token. func NewToken(rawToken string) (*Token, error) { parts := strings.Split(rawToken, TokenSeparator) if len(parts) != 3 { return nil, ErrMalformedToken } var ( rawHeader, rawClaims = parts[0], parts[1] headerJSON, claimsJSON []byte err error ) defer func() { if err != nil { log.Infof("error while unmarshalling raw token: %s", err) } }() if headerJSON, err = joseBase64UrlDecode(rawHeader); err != nil { err = fmt.Errorf("unable to decode header: %s", err) return nil, ErrMalformedToken } if claimsJSON, err = joseBase64UrlDecode(rawClaims); err != nil { err = fmt.Errorf("unable to decode claims: %s", err) return nil, ErrMalformedToken } token := new(Token) token.Header = new(Header) token.Claims = new(ClaimSet) token.Raw = strings.Join(parts[:2], TokenSeparator) if token.Signature, err = joseBase64UrlDecode(parts[2]); err != nil { err = fmt.Errorf("unable to decode signature: %s", err) return nil, ErrMalformedToken } if err = json.Unmarshal(headerJSON, token.Header); err != nil { return nil, ErrMalformedToken } if err = json.Unmarshal(claimsJSON, token.Claims); err != nil { return nil, ErrMalformedToken } return token, nil } // Verify attempts to verify this token using the given options. // Returns a nil error if the token is valid. func (t *Token) Verify(verifyOpts VerifyOptions) error { // Verify that the Issuer claim is a trusted authority. if !contains(verifyOpts.TrustedIssuers, t.Claims.Issuer) { log.Infof("token from untrusted issuer: %q", t.Claims.Issuer) return ErrInvalidToken } // Verify that the Audience claim is allowed. if !contains(verifyOpts.AcceptedAudiences, t.Claims.Audience) { log.Infof("token intended for another audience: %q", t.Claims.Audience) return ErrInvalidToken } // Verify that the token is currently usable and not expired. currentTime := time.Now() ExpWithLeeway := time.Unix(t.Claims.Expiration, 0).Add(Leeway) if currentTime.After(ExpWithLeeway) { log.Infof("token not to be used after %s - currently %s", ExpWithLeeway, currentTime) return ErrInvalidToken } NotBeforeWithLeeway := time.Unix(t.Claims.NotBefore, 0).Add(-Leeway) if currentTime.Before(NotBeforeWithLeeway) { log.Infof("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime) return ErrInvalidToken } // Verify the token signature. if len(t.Signature) == 0 { log.Info("token has no signature") return ErrInvalidToken } // Verify that the signing key is trusted. signingKey, err := t.VerifySigningKey(verifyOpts) if err != nil { log.Info(err) return ErrInvalidToken } // Finally, verify the signature of the token using the key which signed it. if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil { log.Infof("unable to verify token signature: %s", err) return ErrInvalidToken } return nil } // VerifySigningKey attempts to get the key which was used to sign this token. // The token header should contain either of these 3 fields: // `x5c` - The x509 certificate chain for the signing key. Needs to be // verified. // `jwk` - The JSON Web Key representation of the signing key. // May contain its own `x5c` field which needs to be verified. // `kid` - The unique identifier for the key. This library interprets it // as a libtrust fingerprint. The key itself can be looked up in // the trustedKeys field of the given verify options. // Each of these methods are tried in that order of preference until the // signing key is found or an error is returned. func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey libtrust.PublicKey, err error) { // First attempt to get an x509 certificate chain from the header. var ( x5c = t.Header.X5c rawJWK = t.Header.RawJWK keyID = t.Header.KeyID ) switch { case len(x5c) > 0: signingKey, err = parseAndVerifyCertChain(x5c, verifyOpts.Roots) case rawJWK != nil: signingKey, err = parseAndVerifyRawJWK(rawJWK, verifyOpts) case len(keyID) > 0: signingKey = verifyOpts.TrustedKeys[keyID] if signingKey == nil { err = fmt.Errorf("token signed by untrusted key with ID: %q", keyID) } default: err = errors.New("unable to get token signing key") } return } func parseAndVerifyCertChain(x5c []string, roots *x509.CertPool) (leafKey libtrust.PublicKey, err error) { if len(x5c) == 0 { return nil, errors.New("empty x509 certificate chain") } // Ensure the first element is encoded correctly. leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0]) if err != nil { return nil, fmt.Errorf("unable to decode leaf certificate: %s", err) } // And that it is a valid x509 certificate. leafCert, err := x509.ParseCertificate(leafCertDer) if err != nil { return nil, fmt.Errorf("unable to parse leaf certificate: %s", err) } // The rest of the certificate chain are intermediate certificates. intermediates := x509.NewCertPool() for i := 1; i < len(x5c); i++ { intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i]) if err != nil { return nil, fmt.Errorf("unable to decode intermediate certificate: %s", err) } intermediateCert, err := x509.ParseCertificate(intermediateCertDer) if err != nil { return nil, fmt.Errorf("unable to parse intermediate certificate: %s", err) } intermediates.AddCert(intermediateCert) } verifyOpts := x509.VerifyOptions{ Intermediates: intermediates, Roots: roots, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, } // TODO: this call returns certificate chains which we ignore for now, but // we should check them for revocations if we have the ability later. if _, err = leafCert.Verify(verifyOpts); err != nil { return nil, fmt.Errorf("unable to verify certificate chain: %s", err) } // Get the public key from the leaf certificate. leafCryptoKey, ok := leafCert.PublicKey.(crypto.PublicKey) if !ok { return nil, errors.New("unable to get leaf cert public key value") } leafKey, err = libtrust.FromCryptoPublicKey(leafCryptoKey) if err != nil { return nil, fmt.Errorf("unable to make libtrust public key from leaf certificate: %s", err) } return } func parseAndVerifyRawJWK(rawJWK *json.RawMessage, verifyOpts VerifyOptions) (pubKey libtrust.PublicKey, err error) { pubKey, err = libtrust.UnmarshalPublicKeyJWK([]byte(*rawJWK)) if err != nil { return nil, fmt.Errorf("unable to decode raw JWK value: %s", err) } // Check to see if the key includes a certificate chain. x5cVal, ok := pubKey.GetExtendedField("x5c").([]interface{}) if !ok { // The JWK should be one of the trusted root keys. if _, trusted := verifyOpts.TrustedKeys[pubKey.KeyID()]; !trusted { return nil, errors.New("untrusted JWK with no certificate chain") } // The JWK is one of the trusted keys. return } // Ensure each item in the chain is of the correct type. x5c := make([]string, len(x5cVal)) for i, val := range x5cVal { certString, ok := val.(string) if !ok || len(certString) == 0 { return nil, errors.New("malformed certificate chain") } x5c[i] = certString } // Ensure that the x509 certificate chain can // be verified up to one of our trusted roots. leafKey, err := parseAndVerifyCertChain(x5c, verifyOpts.Roots) if err != nil { return nil, fmt.Errorf("could not verify JWK certificate chain: %s", err) } // Verify that the public key in the leaf cert *is* the signing key. if pubKey.KeyID() != leafKey.KeyID() { return nil, errors.New("leaf certificate public key ID does not match JWK key ID") } return } // accessSet returns a set of actions available for the resource // actions listed in the `access` section of this token. func (t *Token) accessSet() accessSet { if t.Claims == nil { return nil } accessSet := make(accessSet, len(t.Claims.Access)) for _, resourceActions := range t.Claims.Access { resource := auth.Resource{ Type: resourceActions.Type, Name: resourceActions.Name, } set, exists := accessSet[resource] if !exists { set = newActionSet() accessSet[resource] = set } for _, action := range resourceActions.Actions { set.add(action) } } return accessSet } func (t *Token) resources() []auth.Resource { if t.Claims == nil { return nil } resourceSet := map[auth.Resource]struct{}{} for _, resourceActions := range t.Claims.Access { resource := auth.Resource{ Type: resourceActions.Type, Class: resourceActions.Class, Name: resourceActions.Name, } resourceSet[resource] = struct{}{} } resources := make([]auth.Resource, 0, len(resourceSet)) for resource := range resourceSet { resources = append(resources, resource) } return resources } func (t *Token) compactRaw() string { return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature)) } docker-registry-2.6.2~ds1/registry/auth/token/token_test.go000066400000000000000000000313521313450123100241000ustar00rootroot00000000000000package token import ( "crypto" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "io/ioutil" "net/http" "os" "strings" "testing" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/auth" "github.com/docker/libtrust" ) func makeRootKeys(numKeys int) ([]libtrust.PrivateKey, error) { keys := make([]libtrust.PrivateKey, 0, numKeys) for i := 0; i < numKeys; i++ { key, err := libtrust.GenerateECP256PrivateKey() if err != nil { return nil, err } keys = append(keys, key) } return keys, nil } func makeSigningKeyWithChain(rootKey libtrust.PrivateKey, depth int) (libtrust.PrivateKey, error) { if depth == 0 { // Don't need to build a chain. return rootKey, nil } var ( x5c = make([]string, depth) parentKey = rootKey key libtrust.PrivateKey cert *x509.Certificate err error ) for depth > 0 { if key, err = libtrust.GenerateECP256PrivateKey(); err != nil { return nil, err } if cert, err = libtrust.GenerateCACert(parentKey, key); err != nil { return nil, err } depth-- x5c[depth] = base64.StdEncoding.EncodeToString(cert.Raw) parentKey = key } key.AddExtendedField("x5c", x5c) return key, nil } func makeRootCerts(rootKeys []libtrust.PrivateKey) ([]*x509.Certificate, error) { certs := make([]*x509.Certificate, 0, len(rootKeys)) for _, key := range rootKeys { cert, err := libtrust.GenerateCACert(key, key) if err != nil { return nil, err } certs = append(certs, cert) } return certs, nil } func makeTrustedKeyMap(rootKeys []libtrust.PrivateKey) map[string]libtrust.PublicKey { trustedKeys := make(map[string]libtrust.PublicKey, len(rootKeys)) for _, key := range rootKeys { trustedKeys[key.KeyID()] = key.PublicKey() } return trustedKeys } func makeTestToken(issuer, audience string, access []*ResourceActions, rootKey libtrust.PrivateKey, depth int, now time.Time, exp time.Time) (*Token, error) { signingKey, err := makeSigningKeyWithChain(rootKey, depth) if err != nil { return nil, fmt.Errorf("unable to make signing key with chain: %s", err) } var rawJWK json.RawMessage rawJWK, err = signingKey.PublicKey().MarshalJSON() if err != nil { return nil, fmt.Errorf("unable to marshal signing key to JSON: %s", err) } joseHeader := &Header{ Type: "JWT", SigningAlg: "ES256", RawJWK: &rawJWK, } randomBytes := make([]byte, 15) if _, err = rand.Read(randomBytes); err != nil { return nil, fmt.Errorf("unable to read random bytes for jwt id: %s", err) } claimSet := &ClaimSet{ Issuer: issuer, Subject: "foo", Audience: audience, Expiration: exp.Unix(), NotBefore: now.Unix(), IssuedAt: now.Unix(), JWTID: base64.URLEncoding.EncodeToString(randomBytes), Access: access, } var joseHeaderBytes, claimSetBytes []byte if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil { return nil, fmt.Errorf("unable to marshal jose header: %s", err) } if claimSetBytes, err = json.Marshal(claimSet); err != nil { return nil, fmt.Errorf("unable to marshal claim set: %s", err) } encodedJoseHeader := joseBase64UrlEncode(joseHeaderBytes) encodedClaimSet := joseBase64UrlEncode(claimSetBytes) encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet) var signatureBytes []byte if signatureBytes, _, err = signingKey.Sign(strings.NewReader(encodingToSign), crypto.SHA256); err != nil { return nil, fmt.Errorf("unable to sign jwt payload: %s", err) } signature := joseBase64UrlEncode(signatureBytes) tokenString := fmt.Sprintf("%s.%s", encodingToSign, signature) return NewToken(tokenString) } // This test makes 4 tokens with a varying number of intermediate // certificates ranging from no intermediate chain to a length of 3 // intermediates. func TestTokenVerify(t *testing.T) { var ( numTokens = 4 issuer = "test-issuer" audience = "test-audience" access = []*ResourceActions{ { Type: "repository", Name: "foo/bar", Actions: []string{"pull", "push"}, }, } ) rootKeys, err := makeRootKeys(numTokens) if err != nil { t.Fatal(err) } rootCerts, err := makeRootCerts(rootKeys) if err != nil { t.Fatal(err) } rootPool := x509.NewCertPool() for _, rootCert := range rootCerts { rootPool.AddCert(rootCert) } trustedKeys := makeTrustedKeyMap(rootKeys) tokens := make([]*Token, 0, numTokens) for i := 0; i < numTokens; i++ { token, err := makeTestToken(issuer, audience, access, rootKeys[i], i, time.Now(), time.Now().Add(5*time.Minute)) if err != nil { t.Fatal(err) } tokens = append(tokens, token) } verifyOps := VerifyOptions{ TrustedIssuers: []string{issuer}, AcceptedAudiences: []string{audience}, Roots: rootPool, TrustedKeys: trustedKeys, } for _, token := range tokens { if err := token.Verify(verifyOps); err != nil { t.Fatal(err) } } } // This tests that we don't fail tokens with nbf within // the defined leeway in seconds func TestLeeway(t *testing.T) { var ( issuer = "test-issuer" audience = "test-audience" access = []*ResourceActions{ { Type: "repository", Name: "foo/bar", Actions: []string{"pull", "push"}, }, } ) rootKeys, err := makeRootKeys(1) if err != nil { t.Fatal(err) } trustedKeys := makeTrustedKeyMap(rootKeys) verifyOps := VerifyOptions{ TrustedIssuers: []string{issuer}, AcceptedAudiences: []string{audience}, Roots: nil, TrustedKeys: trustedKeys, } // nbf verification should pass within leeway futureNow := time.Now().Add(time.Duration(5) * time.Second) token, err := makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute)) if err != nil { t.Fatal(err) } if err := token.Verify(verifyOps); err != nil { t.Fatal(err) } // nbf verification should fail with a skew larger than leeway futureNow = time.Now().Add(time.Duration(61) * time.Second) token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, futureNow, futureNow.Add(5*time.Minute)) if err != nil { t.Fatal(err) } if err = token.Verify(verifyOps); err == nil { t.Fatal("Verification should fail for token with nbf in the future outside leeway") } // exp verification should pass within leeway token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-59*time.Second)) if err != nil { t.Fatal(err) } if err = token.Verify(verifyOps); err != nil { t.Fatal(err) } // exp verification should fail with a skew larger than leeway token, err = makeTestToken(issuer, audience, access, rootKeys[0], 0, time.Now(), time.Now().Add(-60*time.Second)) if err != nil { t.Fatal(err) } if err = token.Verify(verifyOps); err == nil { t.Fatal("Verification should fail for token with exp in the future outside leeway") } } func writeTempRootCerts(rootKeys []libtrust.PrivateKey) (filename string, err error) { rootCerts, err := makeRootCerts(rootKeys) if err != nil { return "", err } tempFile, err := ioutil.TempFile("", "rootCertBundle") if err != nil { return "", err } defer tempFile.Close() for _, cert := range rootCerts { if err = pem.Encode(tempFile, &pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, }); err != nil { os.Remove(tempFile.Name()) return "", err } } return tempFile.Name(), nil } // TestAccessController tests complete integration of the token auth package. // It starts by mocking the options for a token auth accessController which // it creates. It then tries a few mock requests: // - don't supply a token; should error with challenge // - supply an invalid token; should error with challenge // - supply a token with insufficient access; should error with challenge // - supply a valid token; should not error func TestAccessController(t *testing.T) { // Make 2 keys; only the first is to be a trusted root key. rootKeys, err := makeRootKeys(2) if err != nil { t.Fatal(err) } rootCertBundleFilename, err := writeTempRootCerts(rootKeys[:1]) if err != nil { t.Fatal(err) } defer os.Remove(rootCertBundleFilename) realm := "https://auth.example.com/token/" issuer := "test-issuer.example.com" service := "test-service.example.com" options := map[string]interface{}{ "realm": realm, "issuer": issuer, "service": service, "rootcertbundle": rootCertBundleFilename, } accessController, err := newAccessController(options) if err != nil { t.Fatal(err) } // 1. Make a mock http.Request with no token. req, err := http.NewRequest("GET", "http://example.com/foo", nil) if err != nil { t.Fatal(err) } testAccess := auth.Access{ Resource: auth.Resource{ Type: "foo", Name: "bar", }, Action: "baz", } ctx := context.WithRequest(context.Background(), req) authCtx, err := accessController.Authorized(ctx, testAccess) challenge, ok := err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrTokenRequired.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 2. Supply an invalid token. token, err := makeTestToken( issuer, service, []*ResourceActions{{ Type: testAccess.Type, Name: testAccess.Name, Actions: []string{testAccess.Action}, }}, rootKeys[1], 1, time.Now(), time.Now().Add(5*time.Minute), // Everything is valid except the key which signed it. ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) challenge, ok = err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrInvalidToken.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrTokenRequired) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 3. Supply a token with insufficient access. token, err = makeTestToken( issuer, service, []*ResourceActions{}, // No access specified. rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute), ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) challenge, ok = err.(auth.Challenge) if !ok { t.Fatal("accessController did not return a challenge") } if challenge.Error() != ErrInsufficientScope.Error() { t.Fatalf("accessControler did not get expected error - got %s - expected %s", challenge, ErrInsufficientScope) } if authCtx != nil { t.Fatalf("expected nil auth context but got %s", authCtx) } // 4. Supply the token we need, or deserve, or whatever. token, err = makeTestToken( issuer, service, []*ResourceActions{{ Type: testAccess.Type, Name: testAccess.Name, Actions: []string{testAccess.Action}, }}, rootKeys[0], 1, time.Now(), time.Now().Add(5*time.Minute), ) if err != nil { t.Fatal(err) } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.compactRaw())) authCtx, err = accessController.Authorized(ctx, testAccess) if err != nil { t.Fatalf("accessController returned unexpected error: %s", err) } userInfo, ok := authCtx.Value(auth.UserKey).(auth.UserInfo) if !ok { t.Fatal("token accessController did not set auth.user context") } if userInfo.Name != "foo" { t.Fatalf("expected user name %q, got %q", "foo", userInfo.Name) } } // This tests that newAccessController can handle PEM blocks in the certificate // file other than certificates, for example a private key. func TestNewAccessControllerPemBlock(t *testing.T) { rootKeys, err := makeRootKeys(2) if err != nil { t.Fatal(err) } rootCertBundleFilename, err := writeTempRootCerts(rootKeys) if err != nil { t.Fatal(err) } defer os.Remove(rootCertBundleFilename) // Add something other than a certificate to the rootcertbundle file, err := os.OpenFile(rootCertBundleFilename, os.O_WRONLY|os.O_APPEND, 0666) if err != nil { t.Fatal(err) } keyBlock, err := rootKeys[0].PEMBlock() if err != nil { t.Fatal(err) } err = pem.Encode(file, keyBlock) if err != nil { t.Fatal(err) } err = file.Close() if err != nil { t.Fatal(err) } realm := "https://auth.example.com/token/" issuer := "test-issuer.example.com" service := "test-service.example.com" options := map[string]interface{}{ "realm": realm, "issuer": issuer, "service": service, "rootcertbundle": rootCertBundleFilename, } ac, err := newAccessController(options) if err != nil { t.Fatal(err) } if len(ac.(*accessController).rootCerts.Subjects()) != 2 { t.Fatal("accessController has the wrong number of certificates") } } docker-registry-2.6.2~ds1/registry/auth/token/util.go000066400000000000000000000027471313450123100227040ustar00rootroot00000000000000package token import ( "encoding/base64" "errors" "strings" ) // joseBase64UrlEncode encodes the given data using the standard base64 url // encoding format but with all trailing '=' characters omitted in accordance // with the jose specification. // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 func joseBase64UrlEncode(b []byte) string { return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=") } // joseBase64UrlDecode decodes the given string using the standard base64 url // decoder but first adds the appropriate number of trailing '=' characters in // accordance with the jose specification. // http://tools.ietf.org/html/draft-ietf-jose-json-web-signature-31#section-2 func joseBase64UrlDecode(s string) ([]byte, error) { switch len(s) % 4 { case 0: case 2: s += "==" case 3: s += "=" default: return nil, errors.New("illegal base64url string") } return base64.URLEncoding.DecodeString(s) } // actionSet is a special type of stringSet. type actionSet struct { stringSet } func newActionSet(actions ...string) actionSet { return actionSet{newStringSet(actions...)} } // Contains calls StringSet.Contains() for // either "*" or the given action string. func (s actionSet) contains(action string) bool { return s.stringSet.contains("*") || s.stringSet.contains(action) } // contains returns true if q is found in ss. func contains(ss []string, q string) bool { for _, s := range ss { if s == q { return true } } return false } docker-registry-2.6.2~ds1/registry/client/000077500000000000000000000000001313450123100205635ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/client/auth/000077500000000000000000000000001313450123100215245ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/client/auth/api_version.go000066400000000000000000000031701313450123100243720ustar00rootroot00000000000000package auth import ( "net/http" "strings" ) // APIVersion represents a version of an API including its // type and version number. type APIVersion struct { // Type refers to the name of a specific API specification // such as "registry" Type string // Version is the version of the API specification implemented, // This may omit the revision number and only include // the major and minor version, such as "2.0" Version string } // String returns the string formatted API Version func (v APIVersion) String() string { return v.Type + "/" + v.Version } // APIVersions gets the API versions out of an HTTP response using the provided // version header as the key for the HTTP header. func APIVersions(resp *http.Response, versionHeader string) []APIVersion { versions := []APIVersion{} if versionHeader != "" { for _, supportedVersions := range resp.Header[http.CanonicalHeaderKey(versionHeader)] { for _, version := range strings.Fields(supportedVersions) { versions = append(versions, ParseAPIVersion(version)) } } } return versions } // ParseAPIVersion parses an API version string into an APIVersion // Format (Expected, not enforced): // API version string = '/' // API type = [a-z][a-z0-9]* // API version = [0-9]+(\.[0-9]+)? // TODO(dmcgowan): Enforce format, add error condition, remove unknown type func ParseAPIVersion(versionStr string) APIVersion { idx := strings.IndexRune(versionStr, '/') if idx == -1 { return APIVersion{ Type: "unknown", Version: versionStr, } } return APIVersion{ Type: strings.ToLower(versionStr[:idx]), Version: versionStr[idx+1:], } } docker-registry-2.6.2~ds1/registry/client/auth/challenge/000077500000000000000000000000001313450123100234465ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/client/auth/challenge/addr.go000066400000000000000000000012771313450123100247160ustar00rootroot00000000000000package challenge import ( "net/url" "strings" ) // FROM: https://golang.org/src/net/http/http.go // Given a string of the form "host", "host:port", or "[ipv6::address]:port", // return true if the string includes a port. func hasPort(s string) bool { return strings.LastIndex(s, ":") > strings.LastIndex(s, "]") } // FROM: http://golang.org/src/net/http/transport.go var portMap = map[string]string{ "http": "80", "https": "443", } // canonicalAddr returns url.Host but always with a ":port" suffix // FROM: http://golang.org/src/net/http/transport.go func canonicalAddr(url *url.URL) string { addr := url.Host if !hasPort(addr) { return addr + ":" + portMap[url.Scheme] } return addr } docker-registry-2.6.2~ds1/registry/client/auth/challenge/authchallenge.go000066400000000000000000000133031313450123100266010ustar00rootroot00000000000000package challenge import ( "fmt" "net/http" "net/url" "strings" "sync" ) // Challenge carries information from a WWW-Authenticate response header. // See RFC 2617. type Challenge struct { // Scheme is the auth-scheme according to RFC 2617 Scheme string // Parameters are the auth-params according to RFC 2617 Parameters map[string]string } // Manager manages the challenges for endpoints. // The challenges are pulled out of HTTP responses. Only // responses which expect challenges should be added to // the manager, since a non-unauthorized request will be // viewed as not requiring challenges. type Manager interface { // GetChallenges returns the challenges for the given // endpoint URL. GetChallenges(endpoint url.URL) ([]Challenge, error) // AddResponse adds the response to the challenge // manager. The challenges will be parsed out of // the WWW-Authenicate headers and added to the // URL which was produced the response. If the // response was authorized, any challenges for the // endpoint will be cleared. AddResponse(resp *http.Response) error } // NewSimpleManager returns an instance of // Manger which only maps endpoints to challenges // based on the responses which have been added the // manager. The simple manager will make no attempt to // perform requests on the endpoints or cache the responses // to a backend. func NewSimpleManager() Manager { return &simpleManager{ Challanges: make(map[string][]Challenge), } } type simpleManager struct { sync.RWMutex Challanges map[string][]Challenge } func normalizeURL(endpoint *url.URL) { endpoint.Host = strings.ToLower(endpoint.Host) endpoint.Host = canonicalAddr(endpoint) } func (m *simpleManager) GetChallenges(endpoint url.URL) ([]Challenge, error) { normalizeURL(&endpoint) m.RLock() defer m.RUnlock() challenges := m.Challanges[endpoint.String()] return challenges, nil } func (m *simpleManager) AddResponse(resp *http.Response) error { challenges := ResponseChallenges(resp) if resp.Request == nil { return fmt.Errorf("missing request reference") } urlCopy := url.URL{ Path: resp.Request.URL.Path, Host: resp.Request.URL.Host, Scheme: resp.Request.URL.Scheme, } normalizeURL(&urlCopy) m.Lock() defer m.Unlock() m.Challanges[urlCopy.String()] = challenges return nil } // Octet types from RFC 2616. type octetType byte var octetTypes [256]octetType const ( isToken octetType = 1 << iota isSpace ) func init() { // OCTET = // CHAR = // CTL = // CR = // LF = // SP = // HT = // <"> = // CRLF = CR LF // LWS = [CRLF] 1*( SP | HT ) // TEXT = // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT // token = 1* // qdtext = > for c := 0; c < 256; c++ { var t octetType isCtl := c <= 31 || c == 127 isChar := 0 <= c && c <= 127 isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0 if strings.IndexRune(" \t\r\n", rune(c)) >= 0 { t |= isSpace } if isChar && !isCtl && !isSeparator { t |= isToken } octetTypes[c] = t } } // ResponseChallenges returns a list of authorization challenges // for the given http Response. Challenges are only checked if // the response status code was a 401. func ResponseChallenges(resp *http.Response) []Challenge { if resp.StatusCode == http.StatusUnauthorized { // Parse the WWW-Authenticate Header and store the challenges // on this endpoint object. return parseAuthHeader(resp.Header) } return nil } func parseAuthHeader(header http.Header) []Challenge { challenges := []Challenge{} for _, h := range header[http.CanonicalHeaderKey("WWW-Authenticate")] { v, p := parseValueAndParams(h) if v != "" { challenges = append(challenges, Challenge{Scheme: v, Parameters: p}) } } return challenges } func parseValueAndParams(header string) (value string, params map[string]string) { params = make(map[string]string) value, s := expectToken(header) if value == "" { return } value = strings.ToLower(value) s = "," + skipSpace(s) for strings.HasPrefix(s, ",") { var pkey string pkey, s = expectToken(skipSpace(s[1:])) if pkey == "" { return } if !strings.HasPrefix(s, "=") { return } var pvalue string pvalue, s = expectTokenOrQuoted(s[1:]) if pvalue == "" { return } pkey = strings.ToLower(pkey) params[pkey] = pvalue s = skipSpace(s) } return } func skipSpace(s string) (rest string) { i := 0 for ; i < len(s); i++ { if octetTypes[s[i]]&isSpace == 0 { break } } return s[i:] } func expectToken(s string) (token, rest string) { i := 0 for ; i < len(s); i++ { if octetTypes[s[i]]&isToken == 0 { break } } return s[:i], s[i:] } func expectTokenOrQuoted(s string) (value string, rest string) { if !strings.HasPrefix(s, "\"") { return expectToken(s) } s = s[1:] for i := 0; i < len(s); i++ { switch s[i] { case '"': return s[:i], s[i+1:] case '\\': p := make([]byte, len(s)-1) j := copy(p, s[:i]) escape := true for i = i + 1; i < len(s); i++ { b := s[i] switch { case escape: escape = false p[j] = b j++ case b == '\\': escape = true case b == '"': return string(p[:j]), s[i+1:] default: p[j] = b j++ } } return "", "" } } return "", "" } docker-registry-2.6.2~ds1/registry/client/auth/challenge/authchallenge_test.go000066400000000000000000000063031313450123100276420ustar00rootroot00000000000000package challenge import ( "fmt" "net/http" "net/url" "strings" "sync" "testing" ) func TestAuthChallengeParse(t *testing.T) { header := http.Header{} header.Add("WWW-Authenticate", `Bearer realm="https://auth.example.com/token",service="registry.example.com",other=fun,slashed="he\"\l\lo"`) challenges := parseAuthHeader(header) if len(challenges) != 1 { t.Fatalf("Unexpected number of auth challenges: %d, expected 1", len(challenges)) } challenge := challenges[0] if expected := "bearer"; challenge.Scheme != expected { t.Fatalf("Unexpected scheme: %s, expected: %s", challenge.Scheme, expected) } if expected := "https://auth.example.com/token"; challenge.Parameters["realm"] != expected { t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["realm"], expected) } if expected := "registry.example.com"; challenge.Parameters["service"] != expected { t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["service"], expected) } if expected := "fun"; challenge.Parameters["other"] != expected { t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["other"], expected) } if expected := "he\"llo"; challenge.Parameters["slashed"] != expected { t.Fatalf("Unexpected param: %s, expected: %s", challenge.Parameters["slashed"], expected) } } func TestAuthChallengeNormalization(t *testing.T) { testAuthChallengeNormalization(t, "reg.EXAMPLE.com") testAuthChallengeNormalization(t, "bɿɒʜɔiɿ-ɿɘƚƨim-ƚol-ɒ-ƨʞnɒʜƚ.com") testAuthChallengeNormalization(t, "reg.example.com:80") testAuthChallengeConcurrent(t, "reg.EXAMPLE.com") } func testAuthChallengeNormalization(t *testing.T, host string) { scm := NewSimpleManager() url, err := url.Parse(fmt.Sprintf("http://%s/v2/", host)) if err != nil { t.Fatal(err) } resp := &http.Response{ Request: &http.Request{ URL: url, }, Header: make(http.Header), StatusCode: http.StatusUnauthorized, } resp.Header.Add("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"https://%s/token\",service=\"registry.example.com\"", host)) err = scm.AddResponse(resp) if err != nil { t.Fatal(err) } lowered := *url lowered.Host = strings.ToLower(lowered.Host) lowered.Host = canonicalAddr(&lowered) c, err := scm.GetChallenges(lowered) if err != nil { t.Fatal(err) } if len(c) == 0 { t.Fatal("Expected challenge for lower-cased-host URL") } } func testAuthChallengeConcurrent(t *testing.T, host string) { scm := NewSimpleManager() url, err := url.Parse(fmt.Sprintf("http://%s/v2/", host)) if err != nil { t.Fatal(err) } resp := &http.Response{ Request: &http.Request{ URL: url, }, Header: make(http.Header), StatusCode: http.StatusUnauthorized, } resp.Header.Add("WWW-Authenticate", fmt.Sprintf("Bearer realm=\"https://%s/token\",service=\"registry.example.com\"", host)) var s sync.WaitGroup s.Add(2) go func() { defer s.Done() for i := 0; i < 200; i++ { err = scm.AddResponse(resp) if err != nil { t.Error(err) } } }() go func() { defer s.Done() lowered := *url lowered.Host = strings.ToLower(lowered.Host) for k := 0; k < 200; k++ { _, err := scm.GetChallenges(lowered) if err != nil { t.Error(err) } } }() s.Wait() } docker-registry-2.6.2~ds1/registry/client/auth/session.go000066400000000000000000000326321313450123100235440ustar00rootroot00000000000000package auth import ( "encoding/json" "errors" "fmt" "net/http" "net/url" "strings" "sync" "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution/registry/client" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" ) var ( // ErrNoBasicAuthCredentials is returned if a request can't be authorized with // basic auth due to lack of credentials. ErrNoBasicAuthCredentials = errors.New("no basic auth credentials") // ErrNoToken is returned if a request is successful but the body does not // contain an authorization token. ErrNoToken = errors.New("authorization server did not include a token in the response") ) const defaultClientID = "registry-client" // AuthenticationHandler is an interface for authorizing a request from // params from a "WWW-Authenicate" header for a single scheme. type AuthenticationHandler interface { // Scheme returns the scheme as expected from the "WWW-Authenicate" header. Scheme() string // AuthorizeRequest adds the authorization header to a request (if needed) // using the parameters from "WWW-Authenticate" method. The parameters // values depend on the scheme. AuthorizeRequest(req *http.Request, params map[string]string) error } // CredentialStore is an interface for getting credentials for // a given URL type CredentialStore interface { // Basic returns basic auth for the given URL Basic(*url.URL) (string, string) // RefreshToken returns a refresh token for the // given URL and service RefreshToken(*url.URL, string) string // SetRefreshToken sets the refresh token if none // is provided for the given url and service SetRefreshToken(realm *url.URL, service, token string) } // NewAuthorizer creates an authorizer which can handle multiple authentication // schemes. The handlers are tried in order, the higher priority authentication // methods should be first. The challengeMap holds a list of challenges for // a given root API endpoint (for example "https://registry-1.docker.io/v2/"). func NewAuthorizer(manager challenge.Manager, handlers ...AuthenticationHandler) transport.RequestModifier { return &endpointAuthorizer{ challenges: manager, handlers: handlers, } } type endpointAuthorizer struct { challenges challenge.Manager handlers []AuthenticationHandler transport http.RoundTripper } func (ea *endpointAuthorizer) ModifyRequest(req *http.Request) error { pingPath := req.URL.Path if v2Root := strings.Index(req.URL.Path, "/v2/"); v2Root != -1 { pingPath = pingPath[:v2Root+4] } else if v1Root := strings.Index(req.URL.Path, "/v1/"); v1Root != -1 { pingPath = pingPath[:v1Root] + "/v2/" } else { return nil } ping := url.URL{ Host: req.URL.Host, Scheme: req.URL.Scheme, Path: pingPath, } challenges, err := ea.challenges.GetChallenges(ping) if err != nil { return err } if len(challenges) > 0 { for _, handler := range ea.handlers { for _, c := range challenges { if c.Scheme != handler.Scheme() { continue } if err := handler.AuthorizeRequest(req, c.Parameters); err != nil { return err } } } } return nil } // This is the minimum duration a token can last (in seconds). // A token must not live less than 60 seconds because older versions // of the Docker client didn't read their expiration from the token // response and assumed 60 seconds. So to remain compatible with // those implementations, a token must live at least this long. const minimumTokenLifetimeSeconds = 60 // Private interface for time used by this package to enable tests to provide their own implementation. type clock interface { Now() time.Time } type tokenHandler struct { header http.Header creds CredentialStore transport http.RoundTripper clock clock offlineAccess bool forceOAuth bool clientID string scopes []Scope tokenLock sync.Mutex tokenCache string tokenExpiration time.Time } // Scope is a type which is serializable to a string // using the allow scope grammar. type Scope interface { String() string } // RepositoryScope represents a token scope for access // to a repository. type RepositoryScope struct { Repository string Class string Actions []string } // String returns the string representation of the repository // using the scope grammar func (rs RepositoryScope) String() string { repoType := "repository" if rs.Class != "" { repoType = fmt.Sprintf("%s(%s)", repoType, rs.Class) } return fmt.Sprintf("%s:%s:%s", repoType, rs.Repository, strings.Join(rs.Actions, ",")) } // RegistryScope represents a token scope for access // to resources in the registry. type RegistryScope struct { Name string Actions []string } // String returns the string representation of the user // using the scope grammar func (rs RegistryScope) String() string { return fmt.Sprintf("registry:%s:%s", rs.Name, strings.Join(rs.Actions, ",")) } // TokenHandlerOptions is used to configure a new token handler type TokenHandlerOptions struct { Transport http.RoundTripper Credentials CredentialStore OfflineAccess bool ForceOAuth bool ClientID string Scopes []Scope } // An implementation of clock for providing real time data. type realClock struct{} // Now implements clock func (realClock) Now() time.Time { return time.Now() } // NewTokenHandler creates a new AuthenicationHandler which supports // fetching tokens from a remote token server. func NewTokenHandler(transport http.RoundTripper, creds CredentialStore, scope string, actions ...string) AuthenticationHandler { // Create options... return NewTokenHandlerWithOptions(TokenHandlerOptions{ Transport: transport, Credentials: creds, Scopes: []Scope{ RepositoryScope{ Repository: scope, Actions: actions, }, }, }) } // NewTokenHandlerWithOptions creates a new token handler using the provided // options structure. func NewTokenHandlerWithOptions(options TokenHandlerOptions) AuthenticationHandler { handler := &tokenHandler{ transport: options.Transport, creds: options.Credentials, offlineAccess: options.OfflineAccess, forceOAuth: options.ForceOAuth, clientID: options.ClientID, scopes: options.Scopes, clock: realClock{}, } return handler } func (th *tokenHandler) client() *http.Client { return &http.Client{ Transport: th.transport, Timeout: 15 * time.Second, } } func (th *tokenHandler) Scheme() string { return "bearer" } func (th *tokenHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { var additionalScopes []string if fromParam := req.URL.Query().Get("from"); fromParam != "" { additionalScopes = append(additionalScopes, RepositoryScope{ Repository: fromParam, Actions: []string{"pull"}, }.String()) } token, err := th.getToken(params, additionalScopes...) if err != nil { return err } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) return nil } func (th *tokenHandler) getToken(params map[string]string, additionalScopes ...string) (string, error) { th.tokenLock.Lock() defer th.tokenLock.Unlock() scopes := make([]string, 0, len(th.scopes)+len(additionalScopes)) for _, scope := range th.scopes { scopes = append(scopes, scope.String()) } var addedScopes bool for _, scope := range additionalScopes { scopes = append(scopes, scope) addedScopes = true } now := th.clock.Now() if now.After(th.tokenExpiration) || addedScopes { token, expiration, err := th.fetchToken(params, scopes) if err != nil { return "", err } // do not update cache for added scope tokens if !addedScopes { th.tokenCache = token th.tokenExpiration = expiration } return token, nil } return th.tokenCache, nil } type postTokenResponse struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` ExpiresIn int `json:"expires_in"` IssuedAt time.Time `json:"issued_at"` Scope string `json:"scope"` } func (th *tokenHandler) fetchTokenWithOAuth(realm *url.URL, refreshToken, service string, scopes []string) (token string, expiration time.Time, err error) { form := url.Values{} form.Set("scope", strings.Join(scopes, " ")) form.Set("service", service) clientID := th.clientID if clientID == "" { // Use default client, this is a required field clientID = defaultClientID } form.Set("client_id", clientID) if refreshToken != "" { form.Set("grant_type", "refresh_token") form.Set("refresh_token", refreshToken) } else if th.creds != nil { form.Set("grant_type", "password") username, password := th.creds.Basic(realm) form.Set("username", username) form.Set("password", password) // attempt to get a refresh token form.Set("access_type", "offline") } else { // refuse to do oauth without a grant type return "", time.Time{}, fmt.Errorf("no supported grant type") } resp, err := th.client().PostForm(realm.String(), form) if err != nil { return "", time.Time{}, err } defer resp.Body.Close() if !client.SuccessStatus(resp.StatusCode) { err := client.HandleErrorResponse(resp) return "", time.Time{}, err } decoder := json.NewDecoder(resp.Body) var tr postTokenResponse if err = decoder.Decode(&tr); err != nil { return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) } if tr.RefreshToken != "" && tr.RefreshToken != refreshToken { th.creds.SetRefreshToken(realm, service, tr.RefreshToken) } if tr.ExpiresIn < minimumTokenLifetimeSeconds { // The default/minimum lifetime. tr.ExpiresIn = minimumTokenLifetimeSeconds logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) } if tr.IssuedAt.IsZero() { // issued_at is optional in the token response. tr.IssuedAt = th.clock.Now().UTC() } return tr.AccessToken, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil } type getTokenResponse struct { Token string `json:"token"` AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` IssuedAt time.Time `json:"issued_at"` RefreshToken string `json:"refresh_token"` } func (th *tokenHandler) fetchTokenWithBasicAuth(realm *url.URL, service string, scopes []string) (token string, expiration time.Time, err error) { req, err := http.NewRequest("GET", realm.String(), nil) if err != nil { return "", time.Time{}, err } reqParams := req.URL.Query() if service != "" { reqParams.Add("service", service) } for _, scope := range scopes { reqParams.Add("scope", scope) } if th.offlineAccess { reqParams.Add("offline_token", "true") clientID := th.clientID if clientID == "" { clientID = defaultClientID } reqParams.Add("client_id", clientID) } if th.creds != nil { username, password := th.creds.Basic(realm) if username != "" && password != "" { reqParams.Add("account", username) req.SetBasicAuth(username, password) } } req.URL.RawQuery = reqParams.Encode() resp, err := th.client().Do(req) if err != nil { return "", time.Time{}, err } defer resp.Body.Close() if !client.SuccessStatus(resp.StatusCode) { err := client.HandleErrorResponse(resp) return "", time.Time{}, err } decoder := json.NewDecoder(resp.Body) var tr getTokenResponse if err = decoder.Decode(&tr); err != nil { return "", time.Time{}, fmt.Errorf("unable to decode token response: %s", err) } if tr.RefreshToken != "" && th.creds != nil { th.creds.SetRefreshToken(realm, service, tr.RefreshToken) } // `access_token` is equivalent to `token` and if both are specified // the choice is undefined. Canonicalize `access_token` by sticking // things in `token`. if tr.AccessToken != "" { tr.Token = tr.AccessToken } if tr.Token == "" { return "", time.Time{}, ErrNoToken } if tr.ExpiresIn < minimumTokenLifetimeSeconds { // The default/minimum lifetime. tr.ExpiresIn = minimumTokenLifetimeSeconds logrus.Debugf("Increasing token expiration to: %d seconds", tr.ExpiresIn) } if tr.IssuedAt.IsZero() { // issued_at is optional in the token response. tr.IssuedAt = th.clock.Now().UTC() } return tr.Token, tr.IssuedAt.Add(time.Duration(tr.ExpiresIn) * time.Second), nil } func (th *tokenHandler) fetchToken(params map[string]string, scopes []string) (token string, expiration time.Time, err error) { realm, ok := params["realm"] if !ok { return "", time.Time{}, errors.New("no realm specified for token auth challenge") } // TODO(dmcgowan): Handle empty scheme and relative realm realmURL, err := url.Parse(realm) if err != nil { return "", time.Time{}, fmt.Errorf("invalid token auth challenge realm: %s", err) } service := params["service"] var refreshToken string if th.creds != nil { refreshToken = th.creds.RefreshToken(realmURL, service) } if refreshToken != "" || th.forceOAuth { return th.fetchTokenWithOAuth(realmURL, refreshToken, service, scopes) } return th.fetchTokenWithBasicAuth(realmURL, service, scopes) } type basicHandler struct { creds CredentialStore } // NewBasicHandler creaters a new authentiation handler which adds // basic authentication credentials to a request. func NewBasicHandler(creds CredentialStore) AuthenticationHandler { return &basicHandler{ creds: creds, } } func (*basicHandler) Scheme() string { return "basic" } func (bh *basicHandler) AuthorizeRequest(req *http.Request, params map[string]string) error { if bh.creds != nil { username, password := bh.creds.Basic(req.URL) if username != "" && password != "" { req.SetBasicAuth(username, password) return nil } } return ErrNoBasicAuthCredentials } docker-registry-2.6.2~ds1/registry/client/auth/session_test.go000066400000000000000000000630411313450123100246010ustar00rootroot00000000000000package auth import ( "encoding/base64" "fmt" "net/http" "net/http/httptest" "net/url" "testing" "time" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/testutil" ) // An implementation of clock for providing fake time data. type fakeClock struct { current time.Time } // Now implements clock func (fc *fakeClock) Now() time.Time { return fc.current } func testServer(rrm testutil.RequestResponseMap) (string, func()) { h := testutil.NewHandler(rrm) s := httptest.NewServer(h) return s.URL, s.Close } type testAuthenticationWrapper struct { headers http.Header authCheck func(string) bool next http.Handler } func (w *testAuthenticationWrapper) ServeHTTP(rw http.ResponseWriter, r *http.Request) { auth := r.Header.Get("Authorization") if auth == "" || !w.authCheck(auth) { h := rw.Header() for k, values := range w.headers { h[k] = values } rw.WriteHeader(http.StatusUnauthorized) return } w.next.ServeHTTP(rw, r) } func testServerWithAuth(rrm testutil.RequestResponseMap, authenticate string, authCheck func(string) bool) (string, func()) { h := testutil.NewHandler(rrm) wrapper := &testAuthenticationWrapper{ headers: http.Header(map[string][]string{ "X-API-Version": {"registry/2.0"}, "X-Multi-API-Version": {"registry/2.0", "registry/2.1", "trust/1.0"}, "WWW-Authenticate": {authenticate}, }), authCheck: authCheck, next: h, } s := httptest.NewServer(wrapper) return s.URL, s.Close } // ping pings the provided endpoint to determine its required authorization challenges. // If a version header is provided, the versions will be returned. func ping(manager challenge.Manager, endpoint, versionHeader string) ([]APIVersion, error) { resp, err := http.Get(endpoint) if err != nil { return nil, err } defer resp.Body.Close() if err := manager.AddResponse(resp); err != nil { return nil, err } return APIVersions(resp, versionHeader), err } type testCredentialStore struct { username string password string refreshTokens map[string]string } func (tcs *testCredentialStore) Basic(*url.URL) (string, string) { return tcs.username, tcs.password } func (tcs *testCredentialStore) RefreshToken(u *url.URL, service string) string { return tcs.refreshTokens[service] } func (tcs *testCredentialStore) SetRefreshToken(u *url.URL, service string, token string) { if tcs.refreshTokens != nil { tcs.refreshTokens[service] = token } } func TestEndpointAuthorizeToken(t *testing.T) { service := "localhost.localdomain" repo1 := "some/registry" repo2 := "other/registry" scope1 := fmt.Sprintf("repository:%s:pull,push", repo1) scope2 := fmt.Sprintf("repository:%s:pull,push", repo2) tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope1), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"token":"statictoken"}`), }, }, { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?scope=%s&service=%s", url.QueryEscape(scope2), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"token":"badtoken"}`), }, }, }) te, tc := testServer(tokenMap) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) validCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate, validCheck) defer c() challengeManager1 := challenge.NewSimpleManager() versions, err := ping(challengeManager1, e+"/v2/", "x-api-version") if err != nil { t.Fatal(err) } if len(versions) != 1 { t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, nil, repo1, "pull", "push"))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } e2, c2 := testServerWithAuth(m, authenicate, validCheck) defer c2() challengeManager2 := challenge.NewSimpleManager() versions, err = ping(challengeManager2, e2+"/v2/", "x-multi-api-version") if err != nil { t.Fatal(err) } if len(versions) != 3 { t.Fatalf("Unexpected version count: %d, expected 3", len(versions)) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } if check := (APIVersion{Type: "registry", Version: "2.1"}); versions[1] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[1], check) } if check := (APIVersion{Type: "trust", Version: "1.0"}); versions[2] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[2], check) } transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, nil, repo2, "pull", "push"))) client2 := &http.Client{Transport: transport2} req, _ = http.NewRequest("GET", e2+"/v2/hello", nil) resp, err = client2.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) } } func TestEndpointAuthorizeRefreshToken(t *testing.T) { service := "localhost.localdomain" repo1 := "some/registry" repo2 := "other/registry" scope1 := fmt.Sprintf("repository:%s:pull,push", repo1) scope2 := fmt.Sprintf("repository:%s:pull,push", repo2) refreshToken1 := "0123456790abcdef" refreshToken2 := "0123456790fedcba" tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "POST", Route: "/token", Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken1)), }, }, { // In the future this test may fail and require using basic auth to get a different refresh token Request: testutil.Request{ Method: "POST", Route: "/token", Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope2), service)), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken2)), }, }, { Request: testutil.Request{ Method: "POST", Route: "/token", Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken2, url.QueryEscape(scope2), service)), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"access_token":"badtoken","refresh_token":"%s"}`), }, }, }) te, tc := testServer(tokenMap) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) validCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate, validCheck) defer c() challengeManager1 := challenge.NewSimpleManager() versions, err := ping(challengeManager1, e+"/v2/", "x-api-version") if err != nil { t.Fatal(err) } if len(versions) != 1 { t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } creds := &testCredentialStore{ refreshTokens: map[string]string{ service: refreshToken1, }, } transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandler(nil, creds, repo1, "pull", "push"))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } // Try with refresh token setting e2, c2 := testServerWithAuth(m, authenicate, validCheck) defer c2() challengeManager2 := challenge.NewSimpleManager() versions, err = ping(challengeManager2, e2+"/v2/", "x-api-version") if err != nil { t.Fatal(err) } if len(versions) != 1 { t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } transport2 := transport.NewTransport(nil, NewAuthorizer(challengeManager2, NewTokenHandler(nil, creds, repo2, "pull", "push"))) client2 := &http.Client{Transport: transport2} req, _ = http.NewRequest("GET", e2+"/v2/hello", nil) resp, err = client2.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) } if creds.refreshTokens[service] != refreshToken2 { t.Fatalf("Refresh token not set after change") } // Try with bad token e3, c3 := testServerWithAuth(m, authenicate, validCheck) defer c3() challengeManager3 := challenge.NewSimpleManager() versions, err = ping(challengeManager3, e3+"/v2/", "x-api-version") if err != nil { t.Fatal(err) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } transport3 := transport.NewTransport(nil, NewAuthorizer(challengeManager3, NewTokenHandler(nil, creds, repo2, "pull", "push"))) client3 := &http.Client{Transport: transport3} req, _ = http.NewRequest("GET", e3+"/v2/hello", nil) resp, err = client3.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusUnauthorized) } } func TestEndpointAuthorizeV2RefreshToken(t *testing.T) { service := "localhost.localdomain" scope1 := "registry:catalog:search" refreshToken1 := "0123456790abcdef" tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "POST", Route: "/token", Body: []byte(fmt.Sprintf("client_id=registry-client&grant_type=refresh_token&refresh_token=%s&scope=%s&service=%s", refreshToken1, url.QueryEscape(scope1), service)), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(fmt.Sprintf(`{"access_token":"statictoken","refresh_token":"%s"}`, refreshToken1)), }, }, }) te, tc := testServer(tokenMap) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v1/search", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) validCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate, validCheck) defer c() challengeManager1 := challenge.NewSimpleManager() versions, err := ping(challengeManager1, e+"/v2/", "x-api-version") if err != nil { t.Fatal(err) } if len(versions) != 1 { t.Fatalf("Unexpected version count: %d, expected 1", len(versions)) } if check := (APIVersion{Type: "registry", Version: "2.0"}); versions[0] != check { t.Fatalf("Unexpected api version: %#v, expected %#v", versions[0], check) } tho := TokenHandlerOptions{ Credentials: &testCredentialStore{ refreshTokens: map[string]string{ service: refreshToken1, }, }, Scopes: []Scope{ RegistryScope{ Name: "catalog", Actions: []string{"search"}, }, }, } transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager1, NewTokenHandlerWithOptions(tho))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v1/search", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } } func basicAuth(username, password string) string { auth := username + ":" + password return base64.StdEncoding.EncodeToString([]byte(auth)) } func TestEndpointAuthorizeTokenBasic(t *testing.T) { service := "localhost.localdomain" repo := "some/fun/registry" scope := fmt.Sprintf("repository:%s:pull,push", repo) username := "tokenuser" password := "superSecretPa$$word" tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"access_token":"statictoken"}`), }, }, }) authenicate1 := fmt.Sprintf("Basic realm=localhost") basicCheck := func(a string) bool { return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) } te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) bearerCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate2, bearerCheck) defer c() creds := &testCredentialStore{ username: username, password: password, } challengeManager := challenge.NewSimpleManager() _, err := ping(challengeManager, e+"/v2/", "") if err != nil { t.Fatal(err) } transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewTokenHandler(nil, creds, repo, "pull", "push"), NewBasicHandler(creds))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } } func TestEndpointAuthorizeTokenBasicWithExpiresIn(t *testing.T) { service := "localhost.localdomain" repo := "some/fun/registry" scope := fmt.Sprintf("repository:%s:pull,push", repo) username := "tokenuser" password := "superSecretPa$$word" tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"token":"statictoken", "expires_in": 3001}`), }, }, { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"access_token":"statictoken", "expires_in": 3001}`), }, }, }) authenicate1 := fmt.Sprintf("Basic realm=localhost") tokenExchanges := 0 basicCheck := func(a string) bool { tokenExchanges = tokenExchanges + 1 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) } te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) bearerCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate2, bearerCheck) defer c() creds := &testCredentialStore{ username: username, password: password, } challengeManager := challenge.NewSimpleManager() _, err := ping(challengeManager, e+"/v2/", "") if err != nil { t.Fatal(err) } clock := &fakeClock{current: time.Now()} options := TokenHandlerOptions{ Transport: nil, Credentials: creds, Scopes: []Scope{ RepositoryScope{ Repository: repo, Actions: []string{"pull", "push"}, }, }, } tHandler := NewTokenHandlerWithOptions(options) tHandler.(*tokenHandler).clock = clock transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds))) client := &http.Client{Transport: transport1} // First call should result in a token exchange // Subsequent calls should recycle the token from the first request, until the expiration has lapsed. timeIncrement := 1000 * time.Second for i := 0; i < 4; i++ { req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } if tokenExchanges != 1 { t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i) } clock.current = clock.current.Add(timeIncrement) } // After we've exceeded the expiration, we should see a second token exchange. req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } if tokenExchanges != 2 { t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges) } } func TestEndpointAuthorizeTokenBasicWithExpiresInAndIssuedAt(t *testing.T) { service := "localhost.localdomain" repo := "some/fun/registry" scope := fmt.Sprintf("repository:%s:pull,push", repo) username := "tokenuser" password := "superSecretPa$$word" // This test sets things up such that the token was issued one increment // earlier than its sibling in TestEndpointAuthorizeTokenBasicWithExpiresIn. // This will mean that the token expires after 3 increments instead of 4. clock := &fakeClock{current: time.Now()} timeIncrement := 1000 * time.Second firstIssuedAt := clock.Now() clock.current = clock.current.Add(timeIncrement) secondIssuedAt := clock.current.Add(2 * timeIncrement) tokenMap := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"token":"statictoken", "issued_at": "` + firstIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`), }, }, { Request: testutil.Request{ Method: "GET", Route: fmt.Sprintf("/token?account=%s&scope=%s&service=%s", username, url.QueryEscape(scope), service), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: []byte(`{"access_token":"statictoken", "issued_at": "` + secondIssuedAt.Format(time.RFC3339Nano) + `", "expires_in": 3001}`), }, }, }) authenicate1 := fmt.Sprintf("Basic realm=localhost") tokenExchanges := 0 basicCheck := func(a string) bool { tokenExchanges = tokenExchanges + 1 return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) } te, tc := testServerWithAuth(tokenMap, authenicate1, basicCheck) defer tc() m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) authenicate2 := fmt.Sprintf("Bearer realm=%q,service=%q", te+"/token", service) bearerCheck := func(a string) bool { return a == "Bearer statictoken" } e, c := testServerWithAuth(m, authenicate2, bearerCheck) defer c() creds := &testCredentialStore{ username: username, password: password, } challengeManager := challenge.NewSimpleManager() _, err := ping(challengeManager, e+"/v2/", "") if err != nil { t.Fatal(err) } options := TokenHandlerOptions{ Transport: nil, Credentials: creds, Scopes: []Scope{ RepositoryScope{ Repository: repo, Actions: []string{"pull", "push"}, }, }, } tHandler := NewTokenHandlerWithOptions(options) tHandler.(*tokenHandler).clock = clock transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, tHandler, NewBasicHandler(creds))) client := &http.Client{Transport: transport1} // First call should result in a token exchange // Subsequent calls should recycle the token from the first request, until the expiration has lapsed. // We shaved one increment off of the equivalent logic in TestEndpointAuthorizeTokenBasicWithExpiresIn // so this loop should have one fewer iteration. for i := 0; i < 3; i++ { req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } if tokenExchanges != 1 { t.Fatalf("Unexpected number of token exchanges, want: 1, got %d (iteration: %d)", tokenExchanges, i) } clock.current = clock.current.Add(timeIncrement) } // After we've exceeded the expiration, we should see a second token exchange. req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } if tokenExchanges != 2 { t.Fatalf("Unexpected number of token exchanges, want: 2, got %d", tokenExchanges) } } func TestEndpointAuthorizeBasic(t *testing.T) { m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/hello", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, }, }, }) username := "user1" password := "funSecretPa$$word" authenicate := fmt.Sprintf("Basic realm=localhost") validCheck := func(a string) bool { return a == fmt.Sprintf("Basic %s", basicAuth(username, password)) } e, c := testServerWithAuth(m, authenicate, validCheck) defer c() creds := &testCredentialStore{ username: username, password: password, } challengeManager := challenge.NewSimpleManager() _, err := ping(challengeManager, e+"/v2/", "") if err != nil { t.Fatal(err) } transport1 := transport.NewTransport(nil, NewAuthorizer(challengeManager, NewBasicHandler(creds))) client := &http.Client{Transport: transport1} req, _ := http.NewRequest("GET", e+"/v2/hello", nil) resp, err := client.Do(req) if err != nil { t.Fatalf("Error sending get request: %s", err) } if resp.StatusCode != http.StatusAccepted { t.Fatalf("Unexpected status code: %d, expected %d", resp.StatusCode, http.StatusAccepted) } } docker-registry-2.6.2~ds1/registry/client/blob_writer.go000066400000000000000000000073071313450123100234330ustar00rootroot00000000000000package client import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" ) type httpBlobUpload struct { statter distribution.BlobStatter client *http.Client uuid string startedAt time.Time location string // always the last value of the location header. offset int64 closed bool } func (hbu *httpBlobUpload) Reader() (io.ReadCloser, error) { panic("Not implemented") } func (hbu *httpBlobUpload) handleErrorResponse(resp *http.Response) error { if resp.StatusCode == http.StatusNotFound { return distribution.ErrBlobUploadUnknown } return HandleErrorResponse(resp) } func (hbu *httpBlobUpload) ReadFrom(r io.Reader) (n int64, err error) { req, err := http.NewRequest("PATCH", hbu.location, ioutil.NopCloser(r)) if err != nil { return 0, err } defer req.Body.Close() resp, err := hbu.client.Do(req) if err != nil { return 0, err } if !SuccessStatus(resp.StatusCode) { return 0, hbu.handleErrorResponse(resp) } hbu.uuid = resp.Header.Get("Docker-Upload-UUID") hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location) if err != nil { return 0, err } rng := resp.Header.Get("Range") var start, end int64 if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil { return 0, err } else if n != 2 || end < start { return 0, fmt.Errorf("bad range format: %s", rng) } return (end - start + 1), nil } func (hbu *httpBlobUpload) Write(p []byte) (n int, err error) { req, err := http.NewRequest("PATCH", hbu.location, bytes.NewReader(p)) if err != nil { return 0, err } req.Header.Set("Content-Range", fmt.Sprintf("%d-%d", hbu.offset, hbu.offset+int64(len(p)-1))) req.Header.Set("Content-Length", fmt.Sprintf("%d", len(p))) req.Header.Set("Content-Type", "application/octet-stream") resp, err := hbu.client.Do(req) if err != nil { return 0, err } if !SuccessStatus(resp.StatusCode) { return 0, hbu.handleErrorResponse(resp) } hbu.uuid = resp.Header.Get("Docker-Upload-UUID") hbu.location, err = sanitizeLocation(resp.Header.Get("Location"), hbu.location) if err != nil { return 0, err } rng := resp.Header.Get("Range") var start, end int if n, err := fmt.Sscanf(rng, "%d-%d", &start, &end); err != nil { return 0, err } else if n != 2 || end < start { return 0, fmt.Errorf("bad range format: %s", rng) } return (end - start + 1), nil } func (hbu *httpBlobUpload) Size() int64 { return hbu.offset } func (hbu *httpBlobUpload) ID() string { return hbu.uuid } func (hbu *httpBlobUpload) StartedAt() time.Time { return hbu.startedAt } func (hbu *httpBlobUpload) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) { // TODO(dmcgowan): Check if already finished, if so just fetch req, err := http.NewRequest("PUT", hbu.location, nil) if err != nil { return distribution.Descriptor{}, err } values := req.URL.Query() values.Set("digest", desc.Digest.String()) req.URL.RawQuery = values.Encode() resp, err := hbu.client.Do(req) if err != nil { return distribution.Descriptor{}, err } defer resp.Body.Close() if !SuccessStatus(resp.StatusCode) { return distribution.Descriptor{}, hbu.handleErrorResponse(resp) } return hbu.statter.Stat(ctx, desc.Digest) } func (hbu *httpBlobUpload) Cancel(ctx context.Context) error { req, err := http.NewRequest("DELETE", hbu.location, nil) if err != nil { return err } resp, err := hbu.client.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotFound || SuccessStatus(resp.StatusCode) { return nil } return hbu.handleErrorResponse(resp) } func (hbu *httpBlobUpload) Close() error { hbu.closed = true return nil } docker-registry-2.6.2~ds1/registry/client/blob_writer_test.go000066400000000000000000000125761313450123100244760ustar00rootroot00000000000000package client import ( "bytes" "fmt" "net/http" "testing" "github.com/docker/distribution" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/testutil" ) // Test implements distribution.BlobWriter var _ distribution.BlobWriter = &httpBlobUpload{} func TestUploadReadFrom(t *testing.T) { _, b := newRandomBlob(64) repo := "test/upload/readfrom" locationPath := fmt.Sprintf("/v2/%s/uploads/testid", repo) m := testutil.RequestResponseMap([]testutil.RequestResponseMapping{ { Request: testutil.Request{ Method: "GET", Route: "/v2/", }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Docker-Distribution-API-Version": {"registry/2.0"}, }), }, }, // Test Valid case { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"}, "Location": {locationPath}, "Range": {"0-63"}, }), }, }, // Test invalid range { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Docker-Upload-UUID": {"46603072-7a1b-4b41-98f9-fd8a7da89f9b"}, "Location": {locationPath}, "Range": {""}, }), }, }, // Test 404 { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusNotFound, }, }, // Test 400 valid json { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusBadRequest, Body: []byte(` { "errors": [ { "code": "BLOB_UPLOAD_INVALID", "message": "blob upload invalid", "detail": "more detail" } ] } `), }, }, // Test 400 invalid json { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusBadRequest, Body: []byte("something bad happened"), }, }, // Test 500 { Request: testutil.Request{ Method: "PATCH", Route: locationPath, Body: b, }, Response: testutil.Response{ StatusCode: http.StatusInternalServerError, }, }, }) e, c := testServer(m) defer c() blobUpload := &httpBlobUpload{ client: &http.Client{}, } // Valid case blobUpload.location = e + locationPath n, err := blobUpload.ReadFrom(bytes.NewReader(b)) if err != nil { t.Fatalf("Error calling ReadFrom: %s", err) } if n != 64 { t.Fatalf("Wrong length returned from ReadFrom: %d, expected 64", n) } // Bad range blobUpload.location = e + locationPath _, err = blobUpload.ReadFrom(bytes.NewReader(b)) if err == nil { t.Fatalf("Expected error when bad range received") } // 404 blobUpload.location = e + locationPath _, err = blobUpload.ReadFrom(bytes.NewReader(b)) if err == nil { t.Fatalf("Expected error when not found") } if err != distribution.ErrBlobUploadUnknown { t.Fatalf("Wrong error thrown: %s, expected %s", err, distribution.ErrBlobUploadUnknown) } // 400 valid json blobUpload.location = e + locationPath _, err = blobUpload.ReadFrom(bytes.NewReader(b)) if err == nil { t.Fatalf("Expected error when not found") } if uploadErr, ok := err.(errcode.Errors); !ok { t.Fatalf("Wrong error type %T: %s", err, err) } else if len(uploadErr) != 1 { t.Fatalf("Unexpected number of errors: %d, expected 1", len(uploadErr)) } else { v2Err, ok := uploadErr[0].(errcode.Error) if !ok { t.Fatalf("Not an 'Error' type: %#v", uploadErr[0]) } if v2Err.Code != v2.ErrorCodeBlobUploadInvalid { t.Fatalf("Unexpected error code: %s, expected %d", v2Err.Code.String(), v2.ErrorCodeBlobUploadInvalid) } if expected := "blob upload invalid"; v2Err.Message != expected { t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Message, expected) } if expected := "more detail"; v2Err.Detail.(string) != expected { t.Fatalf("Unexpected error message: %q, expected %q", v2Err.Detail.(string), expected) } } // 400 invalid json blobUpload.location = e + locationPath _, err = blobUpload.ReadFrom(bytes.NewReader(b)) if err == nil { t.Fatalf("Expected error when not found") } if uploadErr, ok := err.(*UnexpectedHTTPResponseError); !ok { t.Fatalf("Wrong error type %T: %s", err, err) } else { respStr := string(uploadErr.Response) if expected := "something bad happened"; respStr != expected { t.Fatalf("Unexpected response string: %s, expected: %s", respStr, expected) } } // 500 blobUpload.location = e + locationPath _, err = blobUpload.ReadFrom(bytes.NewReader(b)) if err == nil { t.Fatalf("Expected error when not found") } if uploadErr, ok := err.(*UnexpectedHTTPStatusError); !ok { t.Fatalf("Wrong error type %T: %s", err, err) } else if expected := "500 " + http.StatusText(http.StatusInternalServerError); uploadErr.Status != expected { t.Fatalf("Unexpected response status: %s, expected %s", uploadErr.Status, expected) } } docker-registry-2.6.2~ds1/registry/client/errors.go000066400000000000000000000100711313450123100224250ustar00rootroot00000000000000package client import ( "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/client/auth/challenge" ) // ErrNoErrorsInBody is returned when an HTTP response body parses to an empty // errcode.Errors slice. var ErrNoErrorsInBody = errors.New("no error details found in HTTP response body") // UnexpectedHTTPStatusError is returned when an unexpected HTTP status is // returned when making a registry api call. type UnexpectedHTTPStatusError struct { Status string } func (e *UnexpectedHTTPStatusError) Error() string { return fmt.Sprintf("received unexpected HTTP status: %s", e.Status) } // UnexpectedHTTPResponseError is returned when an expected HTTP status code // is returned, but the content was unexpected and failed to be parsed. type UnexpectedHTTPResponseError struct { ParseErr error StatusCode int Response []byte } func (e *UnexpectedHTTPResponseError) Error() string { return fmt.Sprintf("error parsing HTTP %d response body: %s: %q", e.StatusCode, e.ParseErr.Error(), string(e.Response)) } func parseHTTPErrorResponse(statusCode int, r io.Reader) error { var errors errcode.Errors body, err := ioutil.ReadAll(r) if err != nil { return err } // For backward compatibility, handle irregularly formatted // messages that contain a "details" field. var detailsErr struct { Details string `json:"details"` } err = json.Unmarshal(body, &detailsErr) if err == nil && detailsErr.Details != "" { switch statusCode { case http.StatusUnauthorized: return errcode.ErrorCodeUnauthorized.WithMessage(detailsErr.Details) case http.StatusTooManyRequests: return errcode.ErrorCodeTooManyRequests.WithMessage(detailsErr.Details) default: return errcode.ErrorCodeUnknown.WithMessage(detailsErr.Details) } } if err := json.Unmarshal(body, &errors); err != nil { return &UnexpectedHTTPResponseError{ ParseErr: err, StatusCode: statusCode, Response: body, } } if len(errors) == 0 { // If there was no error specified in the body, return // UnexpectedHTTPResponseError. return &UnexpectedHTTPResponseError{ ParseErr: ErrNoErrorsInBody, StatusCode: statusCode, Response: body, } } return errors } func makeErrorList(err error) []error { if errL, ok := err.(errcode.Errors); ok { return []error(errL) } return []error{err} } func mergeErrors(err1, err2 error) error { return errcode.Errors(append(makeErrorList(err1), makeErrorList(err2)...)) } // HandleErrorResponse returns error parsed from HTTP response for an // unsuccessful HTTP response code (in the range 400 - 499 inclusive). An // UnexpectedHTTPStatusError returned for response code outside of expected // range. func HandleErrorResponse(resp *http.Response) error { if resp.StatusCode >= 400 && resp.StatusCode < 500 { // Check for OAuth errors within the `WWW-Authenticate` header first // See https://tools.ietf.org/html/rfc6750#section-3 for _, c := range challenge.ResponseChallenges(resp) { if c.Scheme == "bearer" { var err errcode.Error // codes defined at https://tools.ietf.org/html/rfc6750#section-3.1 switch c.Parameters["error"] { case "invalid_token": err.Code = errcode.ErrorCodeUnauthorized case "insufficient_scope": err.Code = errcode.ErrorCodeDenied default: continue } if description := c.Parameters["error_description"]; description != "" { err.Message = description } else { err.Message = err.Code.Message() } return mergeErrors(err, parseHTTPErrorResponse(resp.StatusCode, resp.Body)) } } err := parseHTTPErrorResponse(resp.StatusCode, resp.Body) if uErr, ok := err.(*UnexpectedHTTPResponseError); ok && resp.StatusCode == 401 { return errcode.ErrorCodeUnauthorized.WithDetail(uErr.Response) } return err } return &UnexpectedHTTPStatusError{Status: resp.Status} } // SuccessStatus returns true if the argument is a successful HTTP response // code (in the range 200 - 399 inclusive). func SuccessStatus(status int) bool { return status >= 200 && status <= 399 } docker-registry-2.6.2~ds1/registry/client/errors_test.go000066400000000000000000000063161313450123100234730ustar00rootroot00000000000000package client import ( "bytes" "io" "net/http" "strings" "testing" ) type nopCloser struct { io.Reader } func (nopCloser) Close() error { return nil } func TestHandleErrorResponse401ValidBody(t *testing.T) { json := "{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"action requires authentication\"}]}" response := &http.Response{ Status: "401 Unauthorized", StatusCode: 401, Body: nopCloser{bytes.NewBufferString(json)}, } err := HandleErrorResponse(response) expectedMsg := "unauthorized: action requires authentication" if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } func TestHandleErrorResponse401WithInvalidBody(t *testing.T) { json := "{invalid json}" response := &http.Response{ Status: "401 Unauthorized", StatusCode: 401, Body: nopCloser{bytes.NewBufferString(json)}, } err := HandleErrorResponse(response) expectedMsg := "unauthorized: authentication required" if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } func TestHandleErrorResponseExpectedStatusCode400ValidBody(t *testing.T) { json := "{\"errors\":[{\"code\":\"DIGEST_INVALID\",\"message\":\"provided digest does not match\"}]}" response := &http.Response{ Status: "400 Bad Request", StatusCode: 400, Body: nopCloser{bytes.NewBufferString(json)}, } err := HandleErrorResponse(response) expectedMsg := "digest invalid: provided digest does not match" if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } func TestHandleErrorResponseExpectedStatusCode404EmptyErrorSlice(t *testing.T) { json := `{"randomkey": "randomvalue"}` response := &http.Response{ Status: "404 Not Found", StatusCode: 404, Body: nopCloser{bytes.NewBufferString(json)}, } err := HandleErrorResponse(response) expectedMsg := `error parsing HTTP 404 response body: no error details found in HTTP response body: "{\"randomkey\": \"randomvalue\"}"` if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } func TestHandleErrorResponseExpectedStatusCode404InvalidBody(t *testing.T) { json := "{invalid json}" response := &http.Response{ Status: "404 Not Found", StatusCode: 404, Body: nopCloser{bytes.NewBufferString(json)}, } err := HandleErrorResponse(response) expectedMsg := "error parsing HTTP 404 response body: invalid character 'i' looking for beginning of object key string: \"{invalid json}\"" if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } func TestHandleErrorResponseUnexpectedStatusCode501(t *testing.T) { response := &http.Response{ Status: "501 Not Implemented", StatusCode: 501, Body: nopCloser{bytes.NewBufferString("{\"Error Encountered\" : \"Function not implemented.\"}")}, } err := HandleErrorResponse(response) expectedMsg := "received unexpected HTTP status: 501 Not Implemented" if !strings.Contains(err.Error(), expectedMsg) { t.Errorf("Expected \"%s\", got: \"%s\"", expectedMsg, err.Error()) } } docker-registry-2.6.2~ds1/registry/client/repository.go000066400000000000000000000510521313450123100233340ustar00rootroot00000000000000package client import ( "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "strconv" "strings" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/storage/cache" "github.com/docker/distribution/registry/storage/cache/memory" ) // Registry provides an interface for calling Repositories, which returns a catalog of repositories. type Registry interface { Repositories(ctx context.Context, repos []string, last string) (n int, err error) } // checkHTTPRedirect is a callback that can manipulate redirected HTTP // requests. It is used to preserve Accept and Range headers. func checkHTTPRedirect(req *http.Request, via []*http.Request) error { if len(via) >= 10 { return errors.New("stopped after 10 redirects") } if len(via) > 0 { for headerName, headerVals := range via[0].Header { if headerName != "Accept" && headerName != "Range" { continue } for _, val := range headerVals { // Don't add to redirected request if redirected // request already has a header with the same // name and value. hasValue := false for _, existingVal := range req.Header[headerName] { if existingVal == val { hasValue = true break } } if !hasValue { req.Header.Add(headerName, val) } } } } return nil } // NewRegistry creates a registry namespace which can be used to get a listing of repositories func NewRegistry(ctx context.Context, baseURL string, transport http.RoundTripper) (Registry, error) { ub, err := v2.NewURLBuilderFromString(baseURL, false) if err != nil { return nil, err } client := &http.Client{ Transport: transport, Timeout: 1 * time.Minute, CheckRedirect: checkHTTPRedirect, } return ®istry{ client: client, ub: ub, context: ctx, }, nil } type registry struct { client *http.Client ub *v2.URLBuilder context context.Context } // Repositories returns a lexigraphically sorted catalog given a base URL. The 'entries' slice will be filled up to the size // of the slice, starting at the value provided in 'last'. The number of entries will be returned along with io.EOF if there // are no more entries func (r *registry) Repositories(ctx context.Context, entries []string, last string) (int, error) { var numFilled int var returnErr error values := buildCatalogValues(len(entries), last) u, err := r.ub.BuildCatalogURL(values) if err != nil { return 0, err } resp, err := r.client.Get(u) if err != nil { return 0, err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { var ctlg struct { Repositories []string `json:"repositories"` } decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&ctlg); err != nil { return 0, err } for cnt := range ctlg.Repositories { entries[cnt] = ctlg.Repositories[cnt] } numFilled = len(ctlg.Repositories) link := resp.Header.Get("Link") if link == "" { returnErr = io.EOF } } else { return 0, HandleErrorResponse(resp) } return numFilled, returnErr } // NewRepository creates a new Repository for the given repository name and base URL. func NewRepository(ctx context.Context, name reference.Named, baseURL string, transport http.RoundTripper) (distribution.Repository, error) { ub, err := v2.NewURLBuilderFromString(baseURL, false) if err != nil { return nil, err } client := &http.Client{ Transport: transport, CheckRedirect: checkHTTPRedirect, // TODO(dmcgowan): create cookie jar } return &repository{ client: client, ub: ub, name: name, context: ctx, }, nil } type repository struct { client *http.Client ub *v2.URLBuilder context context.Context name reference.Named } func (r *repository) Named() reference.Named { return r.name } func (r *repository) Blobs(ctx context.Context) distribution.BlobStore { statter := &blobStatter{ name: r.name, ub: r.ub, client: r.client, } return &blobs{ name: r.name, ub: r.ub, client: r.client, statter: cache.NewCachedBlobStatter(memory.NewInMemoryBlobDescriptorCacheProvider(), statter), } } func (r *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { // todo(richardscothern): options should be sent over the wire return &manifests{ name: r.name, ub: r.ub, client: r.client, etags: make(map[string]string), }, nil } func (r *repository) Tags(ctx context.Context) distribution.TagService { return &tags{ client: r.client, ub: r.ub, context: r.context, name: r.Named(), } } // tags implements remote tagging operations. type tags struct { client *http.Client ub *v2.URLBuilder context context.Context name reference.Named } // All returns all tags func (t *tags) All(ctx context.Context) ([]string, error) { var tags []string u, err := t.ub.BuildTagsURL(t.name) if err != nil { return tags, err } for { resp, err := t.client.Get(u) if err != nil { return tags, err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { b, err := ioutil.ReadAll(resp.Body) if err != nil { return tags, err } tagsResponse := struct { Tags []string `json:"tags"` }{} if err := json.Unmarshal(b, &tagsResponse); err != nil { return tags, err } tags = append(tags, tagsResponse.Tags...) if link := resp.Header.Get("Link"); link != "" { u = strings.Trim(strings.Split(link, ";")[0], "<>") } else { return tags, nil } } else { return tags, HandleErrorResponse(resp) } } } func descriptorFromResponse(response *http.Response) (distribution.Descriptor, error) { desc := distribution.Descriptor{} headers := response.Header ctHeader := headers.Get("Content-Type") if ctHeader == "" { return distribution.Descriptor{}, errors.New("missing or empty Content-Type header") } desc.MediaType = ctHeader digestHeader := headers.Get("Docker-Content-Digest") if digestHeader == "" { bytes, err := ioutil.ReadAll(response.Body) if err != nil { return distribution.Descriptor{}, err } _, desc, err := distribution.UnmarshalManifest(ctHeader, bytes) if err != nil { return distribution.Descriptor{}, err } return desc, nil } dgst, err := digest.ParseDigest(digestHeader) if err != nil { return distribution.Descriptor{}, err } desc.Digest = dgst lengthHeader := headers.Get("Content-Length") if lengthHeader == "" { return distribution.Descriptor{}, errors.New("missing or empty Content-Length header") } length, err := strconv.ParseInt(lengthHeader, 10, 64) if err != nil { return distribution.Descriptor{}, err } desc.Size = length return desc, nil } // Get issues a HEAD request for a Manifest against its named endpoint in order // to construct a descriptor for the tag. If the registry doesn't support HEADing // a manifest, fallback to GET. func (t *tags) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { ref, err := reference.WithTag(t.name, tag) if err != nil { return distribution.Descriptor{}, err } u, err := t.ub.BuildManifestURL(ref) if err != nil { return distribution.Descriptor{}, err } newRequest := func(method string) (*http.Response, error) { req, err := http.NewRequest(method, u, nil) if err != nil { return nil, err } for _, t := range distribution.ManifestMediaTypes() { req.Header.Add("Accept", t) } resp, err := t.client.Do(req) return resp, err } resp, err := newRequest("HEAD") if err != nil { return distribution.Descriptor{}, err } defer resp.Body.Close() switch { case resp.StatusCode >= 200 && resp.StatusCode < 400: return descriptorFromResponse(resp) default: // if the response is an error - there will be no body to decode. // Issue a GET request: // - for data from a server that does not handle HEAD // - to get error details in case of a failure resp, err = newRequest("GET") if err != nil { return distribution.Descriptor{}, err } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 400 { return descriptorFromResponse(resp) } return distribution.Descriptor{}, HandleErrorResponse(resp) } } func (t *tags) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { panic("not implemented") } func (t *tags) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { panic("not implemented") } func (t *tags) Untag(ctx context.Context, tag string) error { panic("not implemented") } type manifests struct { name reference.Named ub *v2.URLBuilder client *http.Client etags map[string]string } func (ms *manifests) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { ref, err := reference.WithDigest(ms.name, dgst) if err != nil { return false, err } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return false, err } resp, err := ms.client.Head(u) if err != nil { return false, err } if SuccessStatus(resp.StatusCode) { return true, nil } else if resp.StatusCode == http.StatusNotFound { return false, nil } return false, HandleErrorResponse(resp) } // AddEtagToTag allows a client to supply an eTag to Get which will be // used for a conditional HTTP request. If the eTag matches, a nil manifest // and ErrManifestNotModified error will be returned. etag is automatically // quoted when added to this map. func AddEtagToTag(tag, etag string) distribution.ManifestServiceOption { return etagOption{tag, etag} } type etagOption struct{ tag, etag string } func (o etagOption) Apply(ms distribution.ManifestService) error { if ms, ok := ms.(*manifests); ok { ms.etags[o.tag] = fmt.Sprintf(`"%s"`, o.etag) return nil } return fmt.Errorf("etag options is a client-only option") } // ReturnContentDigest allows a client to set a the content digest on // a successful request from the 'Docker-Content-Digest' header. This // returned digest is represents the digest which the registry uses // to refer to the content and can be used to delete the content. func ReturnContentDigest(dgst *digest.Digest) distribution.ManifestServiceOption { return contentDigestOption{dgst} } type contentDigestOption struct{ digest *digest.Digest } func (o contentDigestOption) Apply(ms distribution.ManifestService) error { return nil } func (ms *manifests) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { var ( digestOrTag string ref reference.Named err error contentDgst *digest.Digest ) for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { digestOrTag = opt.Tag ref, err = reference.WithTag(ms.name, opt.Tag) if err != nil { return nil, err } } else if opt, ok := option.(contentDigestOption); ok { contentDgst = opt.digest } else { err := option.Apply(ms) if err != nil { return nil, err } } } if digestOrTag == "" { digestOrTag = dgst.String() ref, err = reference.WithDigest(ms.name, dgst) if err != nil { return nil, err } } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return nil, err } req, err := http.NewRequest("GET", u, nil) if err != nil { return nil, err } for _, t := range distribution.ManifestMediaTypes() { req.Header.Add("Accept", t) } if _, ok := ms.etags[digestOrTag]; ok { req.Header.Set("If-None-Match", ms.etags[digestOrTag]) } resp, err := ms.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode == http.StatusNotModified { return nil, distribution.ErrManifestNotModified } else if SuccessStatus(resp.StatusCode) { if contentDgst != nil { dgst, err := digest.ParseDigest(resp.Header.Get("Docker-Content-Digest")) if err == nil { *contentDgst = dgst } } mt := resp.Header.Get("Content-Type") body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } m, _, err := distribution.UnmarshalManifest(mt, body) if err != nil { return nil, err } return m, nil } return nil, HandleErrorResponse(resp) } // Put puts a manifest. A tag can be specified using an options parameter which uses some shared state to hold the // tag name in order to build the correct upload URL. func (ms *manifests) Put(ctx context.Context, m distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { ref := ms.name var tagged bool for _, option := range options { if opt, ok := option.(distribution.WithTagOption); ok { var err error ref, err = reference.WithTag(ref, opt.Tag) if err != nil { return "", err } tagged = true } else { err := option.Apply(ms) if err != nil { return "", err } } } mediaType, p, err := m.Payload() if err != nil { return "", err } if !tagged { // generate a canonical digest and Put by digest _, d, err := distribution.UnmarshalManifest(mediaType, p) if err != nil { return "", err } ref, err = reference.WithDigest(ref, d.Digest) if err != nil { return "", err } } manifestURL, err := ms.ub.BuildManifestURL(ref) if err != nil { return "", err } putRequest, err := http.NewRequest("PUT", manifestURL, bytes.NewReader(p)) if err != nil { return "", err } putRequest.Header.Set("Content-Type", mediaType) resp, err := ms.client.Do(putRequest) if err != nil { return "", err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { dgstHeader := resp.Header.Get("Docker-Content-Digest") dgst, err := digest.ParseDigest(dgstHeader) if err != nil { return "", err } return dgst, nil } return "", HandleErrorResponse(resp) } func (ms *manifests) Delete(ctx context.Context, dgst digest.Digest) error { ref, err := reference.WithDigest(ms.name, dgst) if err != nil { return err } u, err := ms.ub.BuildManifestURL(ref) if err != nil { return err } req, err := http.NewRequest("DELETE", u, nil) if err != nil { return err } resp, err := ms.client.Do(req) if err != nil { return err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { return nil } return HandleErrorResponse(resp) } // todo(richardscothern): Restore interface and implementation with merge of #1050 /*func (ms *manifests) Enumerate(ctx context.Context, manifests []distribution.Manifest, last distribution.Manifest) (n int, err error) { panic("not supported") }*/ type blobs struct { name reference.Named ub *v2.URLBuilder client *http.Client statter distribution.BlobDescriptorService distribution.BlobDeleter } func sanitizeLocation(location, base string) (string, error) { baseURL, err := url.Parse(base) if err != nil { return "", err } locationURL, err := url.Parse(location) if err != nil { return "", err } return baseURL.ResolveReference(locationURL).String(), nil } func (bs *blobs) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { return bs.statter.Stat(ctx, dgst) } func (bs *blobs) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { reader, err := bs.Open(ctx, dgst) if err != nil { return nil, err } defer reader.Close() return ioutil.ReadAll(reader) } func (bs *blobs) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return nil, err } blobURL, err := bs.ub.BuildBlobURL(ref) if err != nil { return nil, err } return transport.NewHTTPReadSeeker(bs.client, blobURL, func(resp *http.Response) error { if resp.StatusCode == http.StatusNotFound { return distribution.ErrBlobUnknown } return HandleErrorResponse(resp) }), nil } func (bs *blobs) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { panic("not implemented") } func (bs *blobs) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { writer, err := bs.Create(ctx) if err != nil { return distribution.Descriptor{}, err } dgstr := digest.Canonical.New() n, err := io.Copy(writer, io.TeeReader(bytes.NewReader(p), dgstr.Hash())) if err != nil { return distribution.Descriptor{}, err } if n < int64(len(p)) { return distribution.Descriptor{}, fmt.Errorf("short copy: wrote %d of %d", n, len(p)) } desc := distribution.Descriptor{ MediaType: mediaType, Size: int64(len(p)), Digest: dgstr.Digest(), } return writer.Commit(ctx, desc) } type optionFunc func(interface{}) error func (f optionFunc) Apply(v interface{}) error { return f(v) } // WithMountFrom returns a BlobCreateOption which designates that the blob should be // mounted from the given canonical reference. func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { return optionFunc(func(v interface{}) error { opts, ok := v.(*distribution.CreateOptions) if !ok { return fmt.Errorf("unexpected options type: %T", v) } opts.Mount.ShouldMount = true opts.Mount.From = ref return nil }) } func (bs *blobs) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { var opts distribution.CreateOptions for _, option := range options { err := option.Apply(&opts) if err != nil { return nil, err } } var values []url.Values if opts.Mount.ShouldMount { values = append(values, url.Values{"from": {opts.Mount.From.Name()}, "mount": {opts.Mount.From.Digest().String()}}) } u, err := bs.ub.BuildBlobUploadURL(bs.name, values...) if err != nil { return nil, err } resp, err := bs.client.Post(u, "", nil) if err != nil { return nil, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusCreated: desc, err := bs.statter.Stat(ctx, opts.Mount.From.Digest()) if err != nil { return nil, err } return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} case http.StatusAccepted: // TODO(dmcgowan): Check for invalid UUID uuid := resp.Header.Get("Docker-Upload-UUID") location, err := sanitizeLocation(resp.Header.Get("Location"), u) if err != nil { return nil, err } return &httpBlobUpload{ statter: bs.statter, client: bs.client, uuid: uuid, startedAt: time.Now(), location: location, }, nil default: return nil, HandleErrorResponse(resp) } } func (bs *blobs) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { panic("not implemented") } func (bs *blobs) Delete(ctx context.Context, dgst digest.Digest) error { return bs.statter.Clear(ctx, dgst) } type blobStatter struct { name reference.Named ub *v2.URLBuilder client *http.Client } func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return distribution.Descriptor{}, err } u, err := bs.ub.BuildBlobURL(ref) if err != nil { return distribution.Descriptor{}, err } resp, err := bs.client.Head(u) if err != nil { return distribution.Descriptor{}, err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { lengthHeader := resp.Header.Get("Content-Length") if lengthHeader == "" { return distribution.Descriptor{}, fmt.Errorf("missing content-length header for request: %s", u) } length, err := strconv.ParseInt(lengthHeader, 10, 64) if err != nil { return distribution.Descriptor{}, fmt.Errorf("error parsing content-length: %v", err) } return distribution.Descriptor{ MediaType: resp.Header.Get("Content-Type"), Size: length, Digest: dgst, }, nil } else if resp.StatusCode == http.StatusNotFound { return distribution.Descriptor{}, distribution.ErrBlobUnknown } return distribution.Descriptor{}, HandleErrorResponse(resp) } func buildCatalogValues(maxEntries int, last string) url.Values { values := url.Values{} if maxEntries > 0 { values.Add("n", strconv.Itoa(maxEntries)) } if last != "" { values.Add("last", last) } return values } func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { ref, err := reference.WithDigest(bs.name, dgst) if err != nil { return err } blobURL, err := bs.ub.BuildBlobURL(ref) if err != nil { return err } req, err := http.NewRequest("DELETE", blobURL, nil) if err != nil { return err } resp, err := bs.client.Do(req) if err != nil { return err } defer resp.Body.Close() if SuccessStatus(resp.StatusCode) { return nil } return HandleErrorResponse(resp) } func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { return nil } docker-registry-2.6.2~ds1/registry/client/repository_test.go000066400000000000000000000765001313450123100244000ustar00rootroot00000000000000package client import ( "bytes" "crypto/rand" "encoding/json" "fmt" "io" "log" "net/http" "net/http/httptest" "strconv" "strings" "testing" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/testutil" "github.com/docker/distribution/uuid" "github.com/docker/libtrust" ) func testServer(rrm testutil.RequestResponseMap) (string, func()) { h := testutil.NewHandler(rrm) s := httptest.NewServer(h) return s.URL, s.Close } func newRandomBlob(size int) (digest.Digest, []byte) { b := make([]byte, size) if n, err := rand.Read(b); err != nil { panic(err) } else if n != size { panic("unable to read enough bytes") } return digest.FromBytes(b), b } func addTestFetch(repo string, dgst digest.Digest, content []byte, m *testutil.RequestResponseMap) { *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: content, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) } func addTestCatalog(route string, content []byte, link string, m *testutil.RequestResponseMap) { headers := map[string][]string{ "Content-Length": {strconv.Itoa(len(content))}, "Content-Type": {"application/json; charset=utf-8"}, } if link != "" { headers["Link"] = append(headers["Link"], link) } *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: route, }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: content, Headers: http.Header(headers), }, }) } func TestBlobDelete(t *testing.T) { dgst, _ := newRandomBlob(1024) var m testutil.RequestResponseMap repo, _ := reference.ParseNamed("test.example.com/repo1") m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "DELETE", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) err = l.Delete(ctx, dgst) if err != nil { t.Errorf("Error deleting blob: %s", err.Error()) } } func TestBlobFetch(t *testing.T) { d1, b1 := newRandomBlob(1024) var m testutil.RequestResponseMap addTestFetch("test.example.com/repo1", d1, b1, &m) e, c := testServer(m) defer c() ctx := context.Background() repo, _ := reference.ParseNamed("test.example.com/repo1") r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) b, err := l.Get(ctx, d1) if err != nil { t.Fatal(err) } if bytes.Compare(b, b1) != 0 { t.Fatalf("Wrong bytes values fetched: [%d]byte != [%d]byte", len(b), len(b1)) } // TODO(dmcgowan): Test for unknown blob case } func TestBlobExistsNoContentLength(t *testing.T) { var m testutil.RequestResponseMap repo, _ := reference.ParseNamed("biff") dgst, content := newRandomBlob(1024) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: content, Headers: http.Header(map[string][]string{ // "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ // "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) _, err = l.Stat(ctx, dgst) if err == nil { t.Fatal(err) } if !strings.Contains(err.Error(), "missing content-length heade") { t.Fatalf("Expected missing content-length error message") } } func TestBlobExists(t *testing.T) { d1, b1 := newRandomBlob(1024) var m testutil.RequestResponseMap addTestFetch("test.example.com/repo1", d1, b1, &m) e, c := testServer(m) defer c() ctx := context.Background() repo, _ := reference.ParseNamed("test.example.com/repo1") r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) stat, err := l.Stat(ctx, d1) if err != nil { t.Fatal(err) } if stat.Digest != d1 { t.Fatalf("Unexpected digest: %s, expected %s", stat.Digest, d1) } if stat.Size != int64(len(b1)) { t.Fatalf("Unexpected length: %d, expected %d", stat.Size, len(b1)) } // TODO(dmcgowan): Test error cases and ErrBlobUnknown case } func TestBlobUploadChunked(t *testing.T) { dgst, b1 := newRandomBlob(1024) var m testutil.RequestResponseMap chunks := [][]byte{ b1[0:256], b1[256:512], b1[512:513], b1[513:1024], } repo, _ := reference.ParseNamed("test.example.com/uploadrepo") uuids := []string{uuid.Generate().String()} m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "POST", Route: "/v2/" + repo.Name() + "/blobs/uploads/", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[0]}, "Docker-Upload-UUID": {uuids[0]}, "Range": {"0-0"}, }), }, }) offset := 0 for i, chunk := range chunks { uuids = append(uuids, uuid.Generate().String()) newOffset := offset + len(chunk) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PATCH", Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i], Body: chunk, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uuids[i+1]}, "Docker-Upload-UUID": {uuids[i+1]}, "Range": {fmt.Sprintf("%d-%d", offset, newOffset-1)}, }), }, }) offset = newOffset } m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PUT", Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uuids[len(uuids)-1], QueryParams: map[string][]string{ "digest": {dgst.String()}, }, }, Response: testutil.Response{ StatusCode: http.StatusCreated, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Docker-Content-Digest": {dgst.String()}, "Content-Range": {fmt.Sprintf("0-%d", offset-1)}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(offset)}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) upload, err := l.Create(ctx) if err != nil { t.Fatal(err) } if upload.ID() != uuids[0] { log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uuids[0]) } for _, chunk := range chunks { n, err := upload.Write(chunk) if err != nil { t.Fatal(err) } if n != len(chunk) { t.Fatalf("Unexpected length returned from write: %d; expected: %d", n, len(chunk)) } } blob, err := upload.Commit(ctx, distribution.Descriptor{ Digest: dgst, Size: int64(len(b1)), }) if err != nil { t.Fatal(err) } if blob.Size != int64(len(b1)) { t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1)) } } func TestBlobUploadMonolithic(t *testing.T) { dgst, b1 := newRandomBlob(1024) var m testutil.RequestResponseMap repo, _ := reference.ParseNamed("test.example.com/uploadrepo") uploadID := uuid.Generate().String() m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "POST", Route: "/v2/" + repo.Name() + "/blobs/uploads/", }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID}, "Docker-Upload-UUID": {uploadID}, "Range": {"0-0"}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PATCH", Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID, Body: b1, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Location": {"/v2/" + repo.Name() + "/blobs/uploads/" + uploadID}, "Docker-Upload-UUID": {uploadID}, "Content-Length": {"0"}, "Docker-Content-Digest": {dgst.String()}, "Range": {fmt.Sprintf("0-%d", len(b1)-1)}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PUT", Route: "/v2/" + repo.Name() + "/blobs/uploads/" + uploadID, QueryParams: map[string][]string{ "digest": {dgst.String()}, }, }, Response: testutil.Response{ StatusCode: http.StatusCreated, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Docker-Content-Digest": {dgst.String()}, "Content-Range": {fmt.Sprintf("0-%d", len(b1)-1)}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(b1))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) upload, err := l.Create(ctx) if err != nil { t.Fatal(err) } if upload.ID() != uploadID { log.Fatalf("Unexpected UUID %s; expected %s", upload.ID(), uploadID) } n, err := upload.ReadFrom(bytes.NewReader(b1)) if err != nil { t.Fatal(err) } if n != int64(len(b1)) { t.Fatalf("Unexpected ReadFrom length: %d; expected: %d", n, len(b1)) } blob, err := upload.Commit(ctx, distribution.Descriptor{ Digest: dgst, Size: int64(len(b1)), }) if err != nil { t.Fatal(err) } if blob.Size != int64(len(b1)) { t.Fatalf("Unexpected blob size: %d; expected: %d", blob.Size, len(b1)) } } func TestBlobMount(t *testing.T) { dgst, content := newRandomBlob(1024) var m testutil.RequestResponseMap repo, _ := reference.ParseNamed("test.example.com/uploadrepo") sourceRepo, _ := reference.ParseNamed("test.example.com/sourcerepo") canonicalRef, _ := reference.WithDigest(sourceRepo, dgst) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "POST", Route: "/v2/" + repo.Name() + "/blobs/uploads/", QueryParams: map[string][]string{"from": {sourceRepo.Name()}, "mount": {dgst.String()}}, }, Response: testutil.Response{ StatusCode: http.StatusCreated, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Location": {"/v2/" + repo.Name() + "/blobs/" + dgst.String()}, "Docker-Content-Digest": {dgst.String()}, }), }, }) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/blobs/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } l := r.Blobs(ctx) bw, err := l.Create(ctx, WithMountFrom(canonicalRef)) if bw != nil { t.Fatalf("Expected blob writer to be nil, was %v", bw) } if ebm, ok := err.(distribution.ErrBlobMounted); ok { if ebm.From.Digest() != dgst { t.Fatalf("Unexpected digest: %s, expected %s", ebm.From.Digest(), dgst) } if ebm.From.Name() != sourceRepo.Name() { t.Fatalf("Unexpected from: %s, expected %s", ebm.From.Name(), sourceRepo) } } else { t.Fatalf("Unexpected error: %v, expected an ErrBlobMounted", err) } } func newRandomSchemaV1Manifest(name reference.Named, tag string, blobCount int) (*schema1.SignedManifest, digest.Digest, []byte) { blobs := make([]schema1.FSLayer, blobCount) history := make([]schema1.History, blobCount) for i := 0; i < blobCount; i++ { dgst, blob := newRandomBlob((i % 5) * 16) blobs[i] = schema1.FSLayer{BlobSum: dgst} history[i] = schema1.History{V1Compatibility: fmt.Sprintf("{\"Hex\": \"%x\"}", blob)} } m := schema1.Manifest{ Name: name.String(), Tag: tag, Architecture: "x86", FSLayers: blobs, History: history, Versioned: manifest.Versioned{ SchemaVersion: 1, }, } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { panic(err) } sm, err := schema1.Sign(&m, pk) if err != nil { panic(err) } return sm, digest.FromBytes(sm.Canonical), sm.Canonical } func addTestManifestWithEtag(repo reference.Named, reference string, content []byte, m *testutil.RequestResponseMap, dgst string) { actualDigest := digest.FromBytes(content) getReqWithEtag := testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/manifests/" + reference, Headers: http.Header(map[string][]string{ "If-None-Match": {fmt.Sprintf(`"%s"`, dgst)}, }), } var getRespWithEtag testutil.Response if actualDigest.String() == dgst { getRespWithEtag = testutil.Response{ StatusCode: http.StatusNotModified, Body: []byte{}, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, "Content-Type": {schema1.MediaTypeSignedManifest}, }), } } else { getRespWithEtag = testutil.Response{ StatusCode: http.StatusOK, Body: content, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, "Content-Type": {schema1.MediaTypeSignedManifest}, }), } } *m = append(*m, testutil.RequestResponseMapping{Request: getReqWithEtag, Response: getRespWithEtag}) } func contentDigestString(mediatype string, content []byte) string { if mediatype == schema1.MediaTypeSignedManifest { m, _, _ := distribution.UnmarshalManifest(mediatype, content) content = m.(*schema1.SignedManifest).Canonical } return digest.Canonical.FromBytes(content).String() } func addTestManifest(repo reference.Named, reference string, mediatype string, content []byte, m *testutil.RequestResponseMap) { *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/manifests/" + reference, }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: content, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, "Content-Type": {mediatype}, "Docker-Content-Digest": {contentDigestString(mediatype, content)}, }), }, }) *m = append(*m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "HEAD", Route: "/v2/" + repo.Name() + "/manifests/" + reference, }, Response: testutil.Response{ StatusCode: http.StatusOK, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(content))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, "Content-Type": {mediatype}, "Docker-Content-Digest": {digest.Canonical.FromBytes(content).String()}, }), }, }) } func checkEqualManifest(m1, m2 *schema1.SignedManifest) error { if m1.Name != m2.Name { return fmt.Errorf("name does not match %q != %q", m1.Name, m2.Name) } if m1.Tag != m2.Tag { return fmt.Errorf("tag does not match %q != %q", m1.Tag, m2.Tag) } if len(m1.FSLayers) != len(m2.FSLayers) { return fmt.Errorf("fs blob length does not match %d != %d", len(m1.FSLayers), len(m2.FSLayers)) } for i := range m1.FSLayers { if m1.FSLayers[i].BlobSum != m2.FSLayers[i].BlobSum { return fmt.Errorf("blobsum does not match %q != %q", m1.FSLayers[i].BlobSum, m2.FSLayers[i].BlobSum) } } if len(m1.History) != len(m2.History) { return fmt.Errorf("history length does not match %d != %d", len(m1.History), len(m2.History)) } for i := range m1.History { if m1.History[i].V1Compatibility != m2.History[i].V1Compatibility { return fmt.Errorf("blobsum does not match %q != %q", m1.History[i].V1Compatibility, m2.History[i].V1Compatibility) } } return nil } func TestV1ManifestFetch(t *testing.T) { ctx := context.Background() repo, _ := reference.ParseNamed("test.example.com/repo") m1, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) var m testutil.RequestResponseMap _, pl, err := m1.Payload() if err != nil { t.Fatal(err) } addTestManifest(repo, dgst.String(), schema1.MediaTypeSignedManifest, pl, &m) addTestManifest(repo, "latest", schema1.MediaTypeSignedManifest, pl, &m) addTestManifest(repo, "badcontenttype", "text/html", pl, &m) e, c := testServer(m) defer c() r, err := NewRepository(context.Background(), repo, e, nil) if err != nil { t.Fatal(err) } ms, err := r.Manifests(ctx) if err != nil { t.Fatal(err) } ok, err := ms.Exists(ctx, dgst) if err != nil { t.Fatal(err) } if !ok { t.Fatal("Manifest does not exist") } manifest, err := ms.Get(ctx, dgst) if err != nil { t.Fatal(err) } v1manifest, ok := manifest.(*schema1.SignedManifest) if !ok { t.Fatalf("Unexpected manifest type from Get: %T", manifest) } if err := checkEqualManifest(v1manifest, m1); err != nil { t.Fatal(err) } var contentDigest digest.Digest manifest, err = ms.Get(ctx, dgst, distribution.WithTag("latest"), ReturnContentDigest(&contentDigest)) if err != nil { t.Fatal(err) } v1manifest, ok = manifest.(*schema1.SignedManifest) if !ok { t.Fatalf("Unexpected manifest type from Get: %T", manifest) } if err = checkEqualManifest(v1manifest, m1); err != nil { t.Fatal(err) } if contentDigest != dgst { t.Fatalf("Unexpected returned content digest %v, expected %v", contentDigest, dgst) } manifest, err = ms.Get(ctx, dgst, distribution.WithTag("badcontenttype")) if err != nil { t.Fatal(err) } v1manifest, ok = manifest.(*schema1.SignedManifest) if !ok { t.Fatalf("Unexpected manifest type from Get: %T", manifest) } if err = checkEqualManifest(v1manifest, m1); err != nil { t.Fatal(err) } } func TestManifestFetchWithEtag(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo/by/tag") _, d1, p1 := newRandomSchemaV1Manifest(repo, "latest", 6) var m testutil.RequestResponseMap addTestManifestWithEtag(repo, "latest", p1, &m, d1.String()) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } ms, err := r.Manifests(ctx) if err != nil { t.Fatal(err) } clientManifestService, ok := ms.(*manifests) if !ok { panic("wrong type for client manifest service") } _, err = clientManifestService.Get(ctx, d1, distribution.WithTag("latest"), AddEtagToTag("latest", d1.String())) if err != distribution.ErrManifestNotModified { t.Fatal(err) } } func TestManifestDelete(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo/delete") _, dgst1, _ := newRandomSchemaV1Manifest(repo, "latest", 6) _, dgst2, _ := newRandomSchemaV1Manifest(repo, "latest", 6) var m testutil.RequestResponseMap m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "DELETE", Route: "/v2/" + repo.Name() + "/manifests/" + dgst1.String(), }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, }), }, }) e, c := testServer(m) defer c() r, err := NewRepository(context.Background(), repo, e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() ms, err := r.Manifests(ctx) if err != nil { t.Fatal(err) } if err := ms.Delete(ctx, dgst1); err != nil { t.Fatal(err) } if err := ms.Delete(ctx, dgst2); err == nil { t.Fatal("Expected error deleting unknown manifest") } // TODO(dmcgowan): Check for specific unknown error } func TestManifestPut(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo/delete") m1, dgst, _ := newRandomSchemaV1Manifest(repo, "other", 6) _, payload, err := m1.Payload() if err != nil { t.Fatal(err) } var m testutil.RequestResponseMap m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PUT", Route: "/v2/" + repo.Name() + "/manifests/other", Body: payload, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Docker-Content-Digest": {dgst.String()}, }), }, }) putDgst := digest.FromBytes(m1.Canonical) m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "PUT", Route: "/v2/" + repo.Name() + "/manifests/" + putDgst.String(), Body: payload, }, Response: testutil.Response{ StatusCode: http.StatusAccepted, Headers: http.Header(map[string][]string{ "Content-Length": {"0"}, "Docker-Content-Digest": {putDgst.String()}, }), }, }) e, c := testServer(m) defer c() r, err := NewRepository(context.Background(), repo, e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() ms, err := r.Manifests(ctx) if err != nil { t.Fatal(err) } if _, err := ms.Put(ctx, m1, distribution.WithTag(m1.Tag)); err != nil { t.Fatal(err) } if _, err := ms.Put(ctx, m1); err != nil { t.Fatal(err) } // TODO(dmcgowan): Check for invalid input error } func TestManifestTags(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo/tags/list") tagsList := []byte(strings.TrimSpace(` { "name": "test.example.com/repo/tags/list", "tags": [ "tag1", "tag2", "funtag" ] } `)) var m testutil.RequestResponseMap for i := 0; i < 3; i++ { m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/tags/list", }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: tagsList, Headers: http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(tagsList))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }), }, }) } e, c := testServer(m) defer c() r, err := NewRepository(context.Background(), repo, e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() tagService := r.Tags(ctx) tags, err := tagService.All(ctx) if err != nil { t.Fatal(err) } if len(tags) != 3 { t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags)) } expected := map[string]struct{}{ "tag1": {}, "tag2": {}, "funtag": {}, } for _, t := range tags { delete(expected, t) } if len(expected) != 0 { t.Fatalf("unexpected tags returned: %v", expected) } // TODO(dmcgowan): Check for error cases } func TestObtainsErrorForMissingTag(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo") var m testutil.RequestResponseMap var errors errcode.Errors errors = append(errors, v2.ErrorCodeManifestUnknown.WithDetail("unknown manifest")) errBytes, err := json.Marshal(errors) if err != nil { t.Fatal(err) } m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/manifests/1.0.0", }, Response: testutil.Response{ StatusCode: http.StatusNotFound, Body: errBytes, Headers: http.Header(map[string][]string{ "Content-Type": {"application/json; charset=utf-8"}, }), }, }) e, c := testServer(m) defer c() ctx := context.Background() r, err := NewRepository(ctx, repo, e, nil) if err != nil { t.Fatal(err) } tagService := r.Tags(ctx) _, err = tagService.Get(ctx, "1.0.0") if err == nil { t.Fatalf("Expected an error") } if !strings.Contains(err.Error(), "manifest unknown") { t.Fatalf("Expected unknown manifest error message") } } func TestManifestTagsPaginated(t *testing.T) { s := httptest.NewServer(http.NotFoundHandler()) defer s.Close() repo, _ := reference.ParseNamed("test.example.com/repo/tags/list") tagsList := []string{"tag1", "tag2", "funtag"} var m testutil.RequestResponseMap for i := 0; i < 3; i++ { body, err := json.Marshal(map[string]interface{}{ "name": "test.example.com/repo/tags/list", "tags": []string{tagsList[i]}, }) if err != nil { t.Fatal(err) } queryParams := make(map[string][]string) if i > 0 { queryParams["n"] = []string{"1"} queryParams["last"] = []string{tagsList[i-1]} } headers := http.Header(map[string][]string{ "Content-Length": {fmt.Sprint(len(body))}, "Last-Modified": {time.Now().Add(-1 * time.Second).Format(time.ANSIC)}, }) if i < 2 { headers.Set("Link", "<"+s.URL+"/v2/"+repo.Name()+"/tags/list?n=1&last="+tagsList[i]+`>; rel="next"`) } m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/tags/list", QueryParams: queryParams, }, Response: testutil.Response{ StatusCode: http.StatusOK, Body: body, Headers: headers, }, }) } s.Config.Handler = testutil.NewHandler(m) r, err := NewRepository(context.Background(), repo, s.URL, nil) if err != nil { t.Fatal(err) } ctx := context.Background() tagService := r.Tags(ctx) tags, err := tagService.All(ctx) if err != nil { t.Fatal(tags, err) } if len(tags) != 3 { t.Fatalf("Wrong number of tags returned: %d, expected 3", len(tags)) } expected := map[string]struct{}{ "tag1": {}, "tag2": {}, "funtag": {}, } for _, t := range tags { delete(expected, t) } if len(expected) != 0 { t.Fatalf("unexpected tags returned: %v", expected) } } func TestManifestUnauthorized(t *testing.T) { repo, _ := reference.ParseNamed("test.example.com/repo") _, dgst, _ := newRandomSchemaV1Manifest(repo, "latest", 6) var m testutil.RequestResponseMap m = append(m, testutil.RequestResponseMapping{ Request: testutil.Request{ Method: "GET", Route: "/v2/" + repo.Name() + "/manifests/" + dgst.String(), }, Response: testutil.Response{ StatusCode: http.StatusUnauthorized, Body: []byte("garbage"), }, }) e, c := testServer(m) defer c() r, err := NewRepository(context.Background(), repo, e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() ms, err := r.Manifests(ctx) if err != nil { t.Fatal(err) } _, err = ms.Get(ctx, dgst) if err == nil { t.Fatal("Expected error fetching manifest") } v2Err, ok := err.(errcode.Error) if !ok { t.Fatalf("Unexpected error type: %#v", err) } if v2Err.Code != errcode.ErrorCodeUnauthorized { t.Fatalf("Unexpected error code: %s", v2Err.Code.String()) } if expected := errcode.ErrorCodeUnauthorized.Message(); v2Err.Message != expected { t.Fatalf("Unexpected message value: %q, expected %q", v2Err.Message, expected) } } func TestCatalog(t *testing.T) { var m testutil.RequestResponseMap addTestCatalog( "/v2/_catalog?n=5", []byte("{\"repositories\":[\"foo\", \"bar\", \"baz\"]}"), "", &m) e, c := testServer(m) defer c() entries := make([]string, 5) r, err := NewRegistry(context.Background(), e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() numFilled, err := r.Repositories(ctx, entries, "") if err != io.EOF { t.Fatal(err) } if numFilled != 3 { t.Fatalf("Got wrong number of repos") } } func TestCatalogInParts(t *testing.T) { var m testutil.RequestResponseMap addTestCatalog( "/v2/_catalog?n=2", []byte("{\"repositories\":[\"bar\", \"baz\"]}"), "", &m) addTestCatalog( "/v2/_catalog?last=baz&n=2", []byte("{\"repositories\":[\"foo\"]}"), "", &m) e, c := testServer(m) defer c() entries := make([]string, 2) r, err := NewRegistry(context.Background(), e, nil) if err != nil { t.Fatal(err) } ctx := context.Background() numFilled, err := r.Repositories(ctx, entries, "") if err != nil { t.Fatal(err) } if numFilled != 2 { t.Fatalf("Got wrong number of repos") } numFilled, err = r.Repositories(ctx, entries, "baz") if err != io.EOF { t.Fatal(err) } if numFilled != 1 { t.Fatalf("Got wrong number of repos") } } func TestSanitizeLocation(t *testing.T) { for _, testcase := range []struct { description string location string source string expected string err error }{ { description: "ensure relative location correctly resolved", location: "/v2/foo/baasdf", source: "http://blahalaja.com/v1", expected: "http://blahalaja.com/v2/foo/baasdf", }, { description: "ensure parameters are preserved", location: "/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo", source: "http://blahalaja.com/v1", expected: "http://blahalaja.com/v2/foo/baasdf?_state=asdfasfdasdfasdf&digest=foo", }, { description: "ensure new hostname overidden", location: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf", source: "http://blahalaja.com/v1", expected: "https://mwhahaha.com/v2/foo/baasdf?_state=asdfasfdasdfasdf", }, } { fatalf := func(format string, args ...interface{}) { t.Fatalf(testcase.description+": "+format, args...) } s, err := sanitizeLocation(testcase.location, testcase.source) if err != testcase.err { if testcase.err != nil { fatalf("expected error: %v != %v", err, testcase) } else { fatalf("unexpected error sanitizing: %v", err) } } if s != testcase.expected { fatalf("bad sanitize: %q != %q", s, testcase.expected) } } } docker-registry-2.6.2~ds1/registry/client/transport/000077500000000000000000000000001313450123100226175ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/client/transport/http_reader.go000066400000000000000000000143171313450123100254550ustar00rootroot00000000000000package transport import ( "errors" "fmt" "io" "net/http" "os" "regexp" "strconv" ) var ( contentRangeRegexp = regexp.MustCompile(`bytes ([0-9]+)-([0-9]+)/([0-9]+|\\*)`) // ErrWrongCodeForByteRange is returned if the client sends a request // with a Range header but the server returns a 2xx or 3xx code other // than 206 Partial Content. ErrWrongCodeForByteRange = errors.New("expected HTTP 206 from byte range request") ) // ReadSeekCloser combines io.ReadSeeker with io.Closer. type ReadSeekCloser interface { io.ReadSeeker io.Closer } // NewHTTPReadSeeker handles reading from an HTTP endpoint using a GET // request. When seeking and starting a read from a non-zero offset // the a "Range" header will be added which sets the offset. // TODO(dmcgowan): Move this into a separate utility package func NewHTTPReadSeeker(client *http.Client, url string, errorHandler func(*http.Response) error) ReadSeekCloser { return &httpReadSeeker{ client: client, url: url, errorHandler: errorHandler, } } type httpReadSeeker struct { client *http.Client url string // errorHandler creates an error from an unsuccessful HTTP response. // This allows the error to be created with the HTTP response body // without leaking the body through a returned error. errorHandler func(*http.Response) error size int64 // rc is the remote read closer. rc io.ReadCloser // readerOffset tracks the offset as of the last read. readerOffset int64 // seekOffset allows Seek to override the offset. Seek changes // seekOffset instead of changing readOffset directly so that // connection resets can be delayed and possibly avoided if the // seek is undone (i.e. seeking to the end and then back to the // beginning). seekOffset int64 err error } func (hrs *httpReadSeeker) Read(p []byte) (n int, err error) { if hrs.err != nil { return 0, hrs.err } // If we sought to a different position, we need to reset the // connection. This logic is here instead of Seek so that if // a seek is undone before the next read, the connection doesn't // need to be closed and reopened. A common example of this is // seeking to the end to determine the length, and then seeking // back to the original position. if hrs.readerOffset != hrs.seekOffset { hrs.reset() } hrs.readerOffset = hrs.seekOffset rd, err := hrs.reader() if err != nil { return 0, err } n, err = rd.Read(p) hrs.seekOffset += int64(n) hrs.readerOffset += int64(n) return n, err } func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) { if hrs.err != nil { return 0, hrs.err } lastReaderOffset := hrs.readerOffset if whence == os.SEEK_SET && hrs.rc == nil { // If no request has been made yet, and we are seeking to an // absolute position, set the read offset as well to avoid an // unnecessary request. hrs.readerOffset = offset } _, err := hrs.reader() if err != nil { hrs.readerOffset = lastReaderOffset return 0, err } newOffset := hrs.seekOffset switch whence { case os.SEEK_CUR: newOffset += offset case os.SEEK_END: if hrs.size < 0 { return 0, errors.New("content length not known") } newOffset = hrs.size + offset case os.SEEK_SET: newOffset = offset } if newOffset < 0 { err = errors.New("cannot seek to negative position") } else { hrs.seekOffset = newOffset } return hrs.seekOffset, err } func (hrs *httpReadSeeker) Close() error { if hrs.err != nil { return hrs.err } // close and release reader chain if hrs.rc != nil { hrs.rc.Close() } hrs.rc = nil hrs.err = errors.New("httpLayer: closed") return nil } func (hrs *httpReadSeeker) reset() { if hrs.err != nil { return } if hrs.rc != nil { hrs.rc.Close() hrs.rc = nil } } func (hrs *httpReadSeeker) reader() (io.Reader, error) { if hrs.err != nil { return nil, hrs.err } if hrs.rc != nil { return hrs.rc, nil } req, err := http.NewRequest("GET", hrs.url, nil) if err != nil { return nil, err } if hrs.readerOffset > 0 { // If we are at different offset, issue a range request from there. req.Header.Add("Range", fmt.Sprintf("bytes=%d-", hrs.readerOffset)) // TODO: get context in here // context.GetLogger(hrs.context).Infof("Range: %s", req.Header.Get("Range")) } req.Header.Add("Accept-Encoding", "identity") resp, err := hrs.client.Do(req) if err != nil { return nil, err } // Normally would use client.SuccessStatus, but that would be a cyclic // import if resp.StatusCode >= 200 && resp.StatusCode <= 399 { if hrs.readerOffset > 0 { if resp.StatusCode != http.StatusPartialContent { return nil, ErrWrongCodeForByteRange } contentRange := resp.Header.Get("Content-Range") if contentRange == "" { return nil, errors.New("no Content-Range header found in HTTP 206 response") } submatches := contentRangeRegexp.FindStringSubmatch(contentRange) if len(submatches) < 4 { return nil, fmt.Errorf("could not parse Content-Range header: %s", contentRange) } startByte, err := strconv.ParseUint(submatches[1], 10, 64) if err != nil { return nil, fmt.Errorf("could not parse start of range in Content-Range header: %s", contentRange) } if startByte != uint64(hrs.readerOffset) { return nil, fmt.Errorf("received Content-Range starting at offset %d instead of requested %d", startByte, hrs.readerOffset) } endByte, err := strconv.ParseUint(submatches[2], 10, 64) if err != nil { return nil, fmt.Errorf("could not parse end of range in Content-Range header: %s", contentRange) } if submatches[3] == "*" { hrs.size = -1 } else { size, err := strconv.ParseUint(submatches[3], 10, 64) if err != nil { return nil, fmt.Errorf("could not parse total size in Content-Range header: %s", contentRange) } if endByte+1 != size { return nil, fmt.Errorf("range in Content-Range stops before the end of the content: %s", contentRange) } hrs.size = int64(size) } } else if resp.StatusCode == http.StatusOK { hrs.size = resp.ContentLength } else { hrs.size = -1 } hrs.rc = resp.Body } else { defer resp.Body.Close() if hrs.errorHandler != nil { return nil, hrs.errorHandler(resp) } return nil, fmt.Errorf("unexpected status resolving reader: %v", resp.Status) } return hrs.rc, nil } docker-registry-2.6.2~ds1/registry/client/transport/transport.go000066400000000000000000000063571313450123100252150ustar00rootroot00000000000000package transport import ( "io" "net/http" "sync" ) // RequestModifier represents an object which will do an inplace // modification of an HTTP request. type RequestModifier interface { ModifyRequest(*http.Request) error } type headerModifier http.Header // NewHeaderRequestModifier returns a new RequestModifier which will // add the given headers to a request. func NewHeaderRequestModifier(header http.Header) RequestModifier { return headerModifier(header) } func (h headerModifier) ModifyRequest(req *http.Request) error { for k, s := range http.Header(h) { req.Header[k] = append(req.Header[k], s...) } return nil } // NewTransport creates a new transport which will apply modifiers to // the request on a RoundTrip call. func NewTransport(base http.RoundTripper, modifiers ...RequestModifier) http.RoundTripper { return &transport{ Modifiers: modifiers, Base: base, } } // transport is an http.RoundTripper that makes HTTP requests after // copying and modifying the request type transport struct { Modifiers []RequestModifier Base http.RoundTripper mu sync.Mutex // guards modReq modReq map[*http.Request]*http.Request // original -> modified } // RoundTrip authorizes and authenticates the request with an // access token. If no token exists or token is expired, // tries to refresh/fetch a new token. func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { req2 := cloneRequest(req) for _, modifier := range t.Modifiers { if err := modifier.ModifyRequest(req2); err != nil { return nil, err } } t.setModReq(req, req2) res, err := t.base().RoundTrip(req2) if err != nil { t.setModReq(req, nil) return nil, err } res.Body = &onEOFReader{ rc: res.Body, fn: func() { t.setModReq(req, nil) }, } return res, nil } // CancelRequest cancels an in-flight request by closing its connection. func (t *transport) CancelRequest(req *http.Request) { type canceler interface { CancelRequest(*http.Request) } if cr, ok := t.base().(canceler); ok { t.mu.Lock() modReq := t.modReq[req] delete(t.modReq, req) t.mu.Unlock() cr.CancelRequest(modReq) } } func (t *transport) base() http.RoundTripper { if t.Base != nil { return t.Base } return http.DefaultTransport } func (t *transport) setModReq(orig, mod *http.Request) { t.mu.Lock() defer t.mu.Unlock() if t.modReq == nil { t.modReq = make(map[*http.Request]*http.Request) } if mod == nil { delete(t.modReq, orig) } else { t.modReq[orig] = mod } } // cloneRequest returns a clone of the provided *http.Request. // The clone is a shallow copy of the struct and its Header map. func cloneRequest(r *http.Request) *http.Request { // shallow copy of the struct r2 := new(http.Request) *r2 = *r // deep copy of the Header r2.Header = make(http.Header, len(r.Header)) for k, s := range r.Header { r2.Header[k] = append([]string(nil), s...) } return r2 } type onEOFReader struct { rc io.ReadCloser fn func() } func (r *onEOFReader) Read(p []byte) (n int, err error) { n, err = r.rc.Read(p) if err == io.EOF { r.runFunc() } return } func (r *onEOFReader) Close() error { err := r.rc.Close() r.runFunc() return err } func (r *onEOFReader) runFunc() { if fn := r.fn; fn != nil { fn() r.fn = nil } } docker-registry-2.6.2~ds1/registry/doc.go000066400000000000000000000001331313450123100203760ustar00rootroot00000000000000// Package registry provides the main entrypoints for running a registry. package registry docker-registry-2.6.2~ds1/registry/handlers/000077500000000000000000000000001313450123100211055ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/handlers/api_test.go000066400000000000000000002265551313450123100232630ustar00rootroot00000000000000package handlers import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "net/http/httputil" "net/url" "os" "path" "reflect" "regexp" "strconv" "strings" "testing" "github.com/docker/distribution" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" _ "github.com/docker/distribution/registry/storage/driver/testdriver" "github.com/docker/distribution/testutil" "github.com/docker/libtrust" "github.com/gorilla/handlers" ) var headerConfig = http.Header{ "X-Content-Type-Options": []string{"nosniff"}, } // TestCheckAPI hits the base endpoint (/v2/) ensures we return the specified // 200 OK response. func TestCheckAPI(t *testing.T) { env := newTestEnv(t, false) defer env.Shutdown() baseURL, err := env.builder.BuildBaseURL() if err != nil { t.Fatalf("unexpected error building base url: %v", err) } resp, err := http.Get(baseURL) if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() checkResponse(t, "issuing api base check", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Type": []string{"application/json; charset=utf-8"}, "Content-Length": []string{"2"}, }) p, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("unexpected error reading response body: %v", err) } if string(p) != "{}" { t.Fatalf("unexpected response body: %v", string(p)) } } // TestCatalogAPI tests the /v2/_catalog endpoint func TestCatalogAPI(t *testing.T) { chunkLen := 2 env := newTestEnv(t, false) defer env.Shutdown() values := url.Values{ "last": []string{""}, "n": []string{strconv.Itoa(chunkLen)}} catalogURL, err := env.builder.BuildCatalogURL(values) if err != nil { t.Fatalf("unexpected error building catalog url: %v", err) } // ----------------------------------- // try to get an empty catalog resp, err := http.Get(catalogURL) if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() checkResponse(t, "issuing catalog api check", resp, http.StatusOK) var ctlg struct { Repositories []string `json:"repositories"` } dec := json.NewDecoder(resp.Body) if err := dec.Decode(&ctlg); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } // we haven't pushed anything to the registry yet if len(ctlg.Repositories) != 0 { t.Fatalf("repositories has unexpected values") } if resp.Header.Get("Link") != "" { t.Fatalf("repositories has more data when none expected") } // ----------------------------------- // push something to the registry and try again images := []string{"foo/aaaa", "foo/bbbb", "foo/cccc"} for _, image := range images { createRepository(env, t, image, "sometag") } resp, err = http.Get(catalogURL) if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() checkResponse(t, "issuing catalog api check", resp, http.StatusOK) dec = json.NewDecoder(resp.Body) if err = dec.Decode(&ctlg); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } if len(ctlg.Repositories) != chunkLen { t.Fatalf("repositories has unexpected values") } for _, image := range images[:chunkLen] { if !contains(ctlg.Repositories, image) { t.Fatalf("didn't find our repository '%s' in the catalog", image) } } link := resp.Header.Get("Link") if link == "" { t.Fatalf("repositories has less data than expected") } newValues := checkLink(t, link, chunkLen, ctlg.Repositories[len(ctlg.Repositories)-1]) // ----------------------------------- // get the last chunk of data catalogURL, err = env.builder.BuildCatalogURL(newValues) if err != nil { t.Fatalf("unexpected error building catalog url: %v", err) } resp, err = http.Get(catalogURL) if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() checkResponse(t, "issuing catalog api check", resp, http.StatusOK) dec = json.NewDecoder(resp.Body) if err = dec.Decode(&ctlg); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } if len(ctlg.Repositories) != 1 { t.Fatalf("repositories has unexpected values") } lastImage := images[len(images)-1] if !contains(ctlg.Repositories, lastImage) { t.Fatalf("didn't find our repository '%s' in the catalog", lastImage) } link = resp.Header.Get("Link") if link != "" { t.Fatalf("catalog has unexpected data") } } func checkLink(t *testing.T, urlStr string, numEntries int, last string) url.Values { re := regexp.MustCompile("<(/v2/_catalog.*)>; rel=\"next\"") matches := re.FindStringSubmatch(urlStr) if len(matches) != 2 { t.Fatalf("Catalog link address response was incorrect") } linkURL, _ := url.Parse(matches[1]) urlValues := linkURL.Query() if urlValues.Get("n") != strconv.Itoa(numEntries) { t.Fatalf("Catalog link entry size is incorrect") } if urlValues.Get("last") != last { t.Fatal("Catalog link last entry is incorrect") } return urlValues } func contains(elems []string, e string) bool { for _, elem := range elems { if elem == e { return true } } return false } func TestURLPrefix(t *testing.T) { config := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, } config.HTTP.Prefix = "/test/" config.HTTP.Headers = headerConfig env := newTestEnvWithConfig(t, &config) defer env.Shutdown() baseURL, err := env.builder.BuildBaseURL() if err != nil { t.Fatalf("unexpected error building base url: %v", err) } parsed, _ := url.Parse(baseURL) if !strings.HasPrefix(parsed.Path, config.HTTP.Prefix) { t.Fatalf("Prefix %v not included in test url %v", config.HTTP.Prefix, baseURL) } resp, err := http.Get(baseURL) if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() checkResponse(t, "issuing api base check", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Type": []string{"application/json; charset=utf-8"}, "Content-Length": []string{"2"}, }) } type blobArgs struct { imageName reference.Named layerFile io.ReadSeeker layerDigest digest.Digest } func makeBlobArgs(t *testing.T) blobArgs { layerFile, layerDigest, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer file: %v", err) } args := blobArgs{ layerFile: layerFile, layerDigest: layerDigest, } args.imageName, _ = reference.ParseNamed("foo/bar") return args } // TestBlobAPI conducts a full test of the of the blob api. func TestBlobAPI(t *testing.T) { deleteEnabled := false env1 := newTestEnv(t, deleteEnabled) defer env1.Shutdown() args := makeBlobArgs(t) testBlobAPI(t, env1, args) deleteEnabled = true env2 := newTestEnv(t, deleteEnabled) defer env2.Shutdown() args = makeBlobArgs(t) testBlobAPI(t, env2, args) } func TestBlobDelete(t *testing.T) { deleteEnabled := true env := newTestEnv(t, deleteEnabled) defer env.Shutdown() args := makeBlobArgs(t) env = testBlobAPI(t, env, args) testBlobDelete(t, env, args) } func TestRelativeURL(t *testing.T) { config := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, } config.HTTP.Headers = headerConfig config.HTTP.RelativeURLs = false env := newTestEnvWithConfig(t, &config) defer env.Shutdown() ref, _ := reference.WithName("foo/bar") uploadURLBaseAbs, _ := startPushLayer(t, env, ref) u, err := url.Parse(uploadURLBaseAbs) if err != nil { t.Fatal(err) } if !u.IsAbs() { t.Fatal("Relative URL returned from blob upload chunk with non-relative configuration") } args := makeBlobArgs(t) resp, err := doPushLayer(t, env.builder, ref, args.layerDigest, uploadURLBaseAbs, args.layerFile) if err != nil { t.Fatalf("unexpected error doing layer push relative url: %v", err) } checkResponse(t, "relativeurl blob upload", resp, http.StatusCreated) u, err = url.Parse(resp.Header.Get("Location")) if err != nil { t.Fatal(err) } if !u.IsAbs() { t.Fatal("Relative URL returned from blob upload with non-relative configuration") } config.HTTP.RelativeURLs = true args = makeBlobArgs(t) uploadURLBaseRelative, _ := startPushLayer(t, env, ref) u, err = url.Parse(uploadURLBaseRelative) if err != nil { t.Fatal(err) } if u.IsAbs() { t.Fatal("Absolute URL returned from blob upload chunk with relative configuration") } // Start a new upload in absolute mode to get a valid base URL config.HTTP.RelativeURLs = false uploadURLBaseAbs, _ = startPushLayer(t, env, ref) u, err = url.Parse(uploadURLBaseAbs) if err != nil { t.Fatal(err) } if !u.IsAbs() { t.Fatal("Relative URL returned from blob upload chunk with non-relative configuration") } // Complete upload with relative URLs enabled to ensure the final location is relative config.HTTP.RelativeURLs = true resp, err = doPushLayer(t, env.builder, ref, args.layerDigest, uploadURLBaseAbs, args.layerFile) if err != nil { t.Fatalf("unexpected error doing layer push relative url: %v", err) } checkResponse(t, "relativeurl blob upload", resp, http.StatusCreated) u, err = url.Parse(resp.Header.Get("Location")) if err != nil { t.Fatal(err) } if u.IsAbs() { t.Fatal("Relative URL returned from blob upload with non-relative configuration") } } func TestBlobDeleteDisabled(t *testing.T) { deleteEnabled := false env := newTestEnv(t, deleteEnabled) defer env.Shutdown() args := makeBlobArgs(t) imageName := args.imageName layerDigest := args.layerDigest ref, _ := reference.WithDigest(imageName, layerDigest) layerURL, err := env.builder.BuildBlobURL(ref) if err != nil { t.Fatalf("error building url: %v", err) } resp, err := httpDelete(layerURL) if err != nil { t.Fatalf("unexpected error deleting when disabled: %v", err) } checkResponse(t, "status of disabled delete", resp, http.StatusMethodNotAllowed) } func testBlobAPI(t *testing.T, env *testEnv, args blobArgs) *testEnv { // TODO(stevvooe): This test code is complete junk but it should cover the // complete flow. This must be broken down and checked against the // specification *before* we submit the final to docker core. imageName := args.imageName layerFile := args.layerFile layerDigest := args.layerDigest // ----------------------------------- // Test fetch for non-existent content ref, _ := reference.WithDigest(imageName, layerDigest) layerURL, err := env.builder.BuildBlobURL(ref) if err != nil { t.Fatalf("error building url: %v", err) } resp, err := http.Get(layerURL) if err != nil { t.Fatalf("unexpected error fetching non-existent layer: %v", err) } checkResponse(t, "fetching non-existent content", resp, http.StatusNotFound) // ------------------------------------------ // Test head request for non-existent content resp, err = http.Head(layerURL) if err != nil { t.Fatalf("unexpected error checking head on non-existent layer: %v", err) } checkResponse(t, "checking head on non-existent layer", resp, http.StatusNotFound) // ------------------------------------------ // Start an upload, check the status then cancel uploadURLBase, uploadUUID := startPushLayer(t, env, imageName) // A status check should work resp, err = http.Get(uploadURLBase) if err != nil { t.Fatalf("unexpected error getting upload status: %v", err) } checkResponse(t, "status of deleted upload", resp, http.StatusNoContent) checkHeaders(t, resp, http.Header{ "Location": []string{"*"}, "Range": []string{"0-0"}, "Docker-Upload-UUID": []string{uploadUUID}, }) req, err := http.NewRequest("DELETE", uploadURLBase, nil) if err != nil { t.Fatalf("unexpected error creating delete request: %v", err) } resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("unexpected error sending delete request: %v", err) } checkResponse(t, "deleting upload", resp, http.StatusNoContent) // A status check should result in 404 resp, err = http.Get(uploadURLBase) if err != nil { t.Fatalf("unexpected error getting upload status: %v", err) } checkResponse(t, "status of deleted upload", resp, http.StatusNotFound) // ----------------------------------------- // Do layer push with an empty body and different digest uploadURLBase, uploadUUID = startPushLayer(t, env, imageName) resp, err = doPushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, bytes.NewReader([]byte{})) if err != nil { t.Fatalf("unexpected error doing bad layer push: %v", err) } checkResponse(t, "bad layer push", resp, http.StatusBadRequest) checkBodyHasErrorCodes(t, "bad layer push", resp, v2.ErrorCodeDigestInvalid) // ----------------------------------------- // Do layer push with an empty body and correct digest zeroDigest, err := digest.FromReader(bytes.NewReader([]byte{})) if err != nil { t.Fatalf("unexpected error digesting empty buffer: %v", err) } uploadURLBase, uploadUUID = startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, zeroDigest, uploadURLBase, bytes.NewReader([]byte{})) // ----------------------------------------- // Do layer push with an empty body and correct digest // This is a valid but empty tarfile! emptyTar := bytes.Repeat([]byte("\x00"), 1024) emptyDigest, err := digest.FromReader(bytes.NewReader(emptyTar)) if err != nil { t.Fatalf("unexpected error digesting empty tar: %v", err) } uploadURLBase, uploadUUID = startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, emptyDigest, uploadURLBase, bytes.NewReader(emptyTar)) // ------------------------------------------ // Now, actually do successful upload. layerLength, _ := layerFile.Seek(0, os.SEEK_END) layerFile.Seek(0, os.SEEK_SET) uploadURLBase, uploadUUID = startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) // ------------------------------------------ // Now, push just a chunk layerFile.Seek(0, 0) canonicalDigester := digest.Canonical.New() if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { t.Fatalf("error copying to digest: %v", err) } canonicalDigest := canonicalDigester.Digest() layerFile.Seek(0, 0) uploadURLBase, uploadUUID = startPushLayer(t, env, imageName) uploadURLBase, dgst := pushChunk(t, env.builder, imageName, uploadURLBase, layerFile, layerLength) finishUpload(t, env.builder, imageName, uploadURLBase, dgst) // ------------------------ // Use a head request to see if the layer exists. resp, err = http.Head(layerURL) if err != nil { t.Fatalf("unexpected error checking head on existing layer: %v", err) } checkResponse(t, "checking head on existing layer", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Length": []string{fmt.Sprint(layerLength)}, "Docker-Content-Digest": []string{canonicalDigest.String()}, }) // ---------------- // Fetch the layer! resp, err = http.Get(layerURL) if err != nil { t.Fatalf("unexpected error fetching layer: %v", err) } checkResponse(t, "fetching layer", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Length": []string{fmt.Sprint(layerLength)}, "Docker-Content-Digest": []string{canonicalDigest.String()}, }) // Verify the body verifier, err := digest.NewDigestVerifier(layerDigest) if err != nil { t.Fatalf("unexpected error getting digest verifier: %s", err) } io.Copy(verifier, resp.Body) if !verifier.Verified() { t.Fatalf("response body did not pass verification") } // ---------------- // Fetch the layer with an invalid digest badURL := strings.Replace(layerURL, "sha256", "sha257", 1) resp, err = http.Get(badURL) if err != nil { t.Fatalf("unexpected error fetching layer: %v", err) } checkResponse(t, "fetching layer bad digest", resp, http.StatusBadRequest) // Cache headers resp, err = http.Get(layerURL) if err != nil { t.Fatalf("unexpected error fetching layer: %v", err) } checkResponse(t, "fetching layer", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Length": []string{fmt.Sprint(layerLength)}, "Docker-Content-Digest": []string{canonicalDigest.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, canonicalDigest)}, "Cache-Control": []string{"max-age=31536000"}, }) // Matching etag, gives 304 etag := resp.Header.Get("Etag") req, err = http.NewRequest("GET", layerURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching layer with etag", resp, http.StatusNotModified) // Non-matching etag, gives 200 req, err = http.NewRequest("GET", layerURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", "") resp, err = http.DefaultClient.Do(req) checkResponse(t, "fetching layer with invalid etag", resp, http.StatusOK) // Missing tests: // - Upload the same tar file under and different repository and // ensure the content remains uncorrupted. return env } func testBlobDelete(t *testing.T, env *testEnv, args blobArgs) { // Upload a layer imageName := args.imageName layerFile := args.layerFile layerDigest := args.layerDigest ref, _ := reference.WithDigest(imageName, layerDigest) layerURL, err := env.builder.BuildBlobURL(ref) if err != nil { t.Fatalf(err.Error()) } // --------------- // Delete a layer resp, err := httpDelete(layerURL) if err != nil { t.Fatalf("unexpected error deleting layer: %v", err) } checkResponse(t, "deleting layer", resp, http.StatusAccepted) checkHeaders(t, resp, http.Header{ "Content-Length": []string{"0"}, }) // --------------- // Try and get it back // Use a head request to see if the layer exists. resp, err = http.Head(layerURL) if err != nil { t.Fatalf("unexpected error checking head on existing layer: %v", err) } checkResponse(t, "checking existence of deleted layer", resp, http.StatusNotFound) // Delete already deleted layer resp, err = httpDelete(layerURL) if err != nil { t.Fatalf("unexpected error deleting layer: %v", err) } checkResponse(t, "deleting layer", resp, http.StatusNotFound) // ---------------- // Attempt to delete a layer with an invalid digest badURL := strings.Replace(layerURL, "sha256", "sha257", 1) resp, err = httpDelete(badURL) if err != nil { t.Fatalf("unexpected error fetching layer: %v", err) } checkResponse(t, "deleting layer bad digest", resp, http.StatusBadRequest) // ---------------- // Reupload previously deleted blob layerFile.Seek(0, os.SEEK_SET) uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) layerFile.Seek(0, os.SEEK_SET) canonicalDigester := digest.Canonical.New() if _, err := io.Copy(canonicalDigester.Hash(), layerFile); err != nil { t.Fatalf("error copying to digest: %v", err) } canonicalDigest := canonicalDigester.Digest() // ------------------------ // Use a head request to see if it exists resp, err = http.Head(layerURL) if err != nil { t.Fatalf("unexpected error checking head on existing layer: %v", err) } layerLength, _ := layerFile.Seek(0, os.SEEK_END) checkResponse(t, "checking head on reuploaded layer", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Content-Length": []string{fmt.Sprint(layerLength)}, "Docker-Content-Digest": []string{canonicalDigest.String()}, }) } func TestDeleteDisabled(t *testing.T) { env := newTestEnv(t, false) defer env.Shutdown() imageName, _ := reference.ParseNamed("foo/bar") // "build" our layer file layerFile, layerDigest, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer file: %v", err) } ref, _ := reference.WithDigest(imageName, layerDigest) layerURL, err := env.builder.BuildBlobURL(ref) if err != nil { t.Fatalf("Error building blob URL") } uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) resp, err := httpDelete(layerURL) if err != nil { t.Fatalf("unexpected error deleting layer: %v", err) } checkResponse(t, "deleting layer with delete disabled", resp, http.StatusMethodNotAllowed) } func TestDeleteReadOnly(t *testing.T) { env := newTestEnv(t, true) defer env.Shutdown() imageName, _ := reference.ParseNamed("foo/bar") // "build" our layer file layerFile, layerDigest, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer file: %v", err) } ref, _ := reference.WithDigest(imageName, layerDigest) layerURL, err := env.builder.BuildBlobURL(ref) if err != nil { t.Fatalf("Error building blob URL") } uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, layerDigest, uploadURLBase, layerFile) env.app.readOnly = true resp, err := httpDelete(layerURL) if err != nil { t.Fatalf("unexpected error deleting layer: %v", err) } checkResponse(t, "deleting layer in read-only mode", resp, http.StatusMethodNotAllowed) } func TestStartPushReadOnly(t *testing.T) { env := newTestEnv(t, true) defer env.Shutdown() env.app.readOnly = true imageName, _ := reference.ParseNamed("foo/bar") layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) if err != nil { t.Fatalf("unexpected error building layer upload url: %v", err) } resp, err := http.Post(layerUploadURL, "", nil) if err != nil { t.Fatalf("unexpected error starting layer push: %v", err) } defer resp.Body.Close() checkResponse(t, "starting push in read-only mode", resp, http.StatusMethodNotAllowed) } func httpDelete(url string) (*http.Response, error) { req, err := http.NewRequest("DELETE", url, nil) if err != nil { return nil, err } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } // defer resp.Body.Close() return resp, err } type manifestArgs struct { imageName reference.Named mediaType string manifest distribution.Manifest dgst digest.Digest } func TestManifestAPI(t *testing.T) { schema1Repo, _ := reference.ParseNamed("foo/schema1") schema2Repo, _ := reference.ParseNamed("foo/schema2") deleteEnabled := false env1 := newTestEnv(t, deleteEnabled) defer env1.Shutdown() testManifestAPISchema1(t, env1, schema1Repo) schema2Args := testManifestAPISchema2(t, env1, schema2Repo) testManifestAPIManifestList(t, env1, schema2Args) deleteEnabled = true env2 := newTestEnv(t, deleteEnabled) defer env2.Shutdown() testManifestAPISchema1(t, env2, schema1Repo) schema2Args = testManifestAPISchema2(t, env2, schema2Repo) testManifestAPIManifestList(t, env2, schema2Args) } func TestManifestDelete(t *testing.T) { schema1Repo, _ := reference.ParseNamed("foo/schema1") schema2Repo, _ := reference.ParseNamed("foo/schema2") deleteEnabled := true env := newTestEnv(t, deleteEnabled) defer env.Shutdown() schema1Args := testManifestAPISchema1(t, env, schema1Repo) testManifestDelete(t, env, schema1Args) schema2Args := testManifestAPISchema2(t, env, schema2Repo) testManifestDelete(t, env, schema2Args) } func TestManifestDeleteDisabled(t *testing.T) { schema1Repo, _ := reference.ParseNamed("foo/schema1") deleteEnabled := false env := newTestEnv(t, deleteEnabled) defer env.Shutdown() testManifestDeleteDisabled(t, env, schema1Repo) } func testManifestDeleteDisabled(t *testing.T, env *testEnv, imageName reference.Named) { ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) manifestURL, err := env.builder.BuildManifestURL(ref) if err != nil { t.Fatalf("unexpected error getting manifest url: %v", err) } resp, err := httpDelete(manifestURL) if err != nil { t.Fatalf("unexpected error deleting manifest %v", err) } defer resp.Body.Close() checkResponse(t, "status of disabled delete of manifest", resp, http.StatusMethodNotAllowed) } func testManifestAPISchema1(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { tag := "thetag" args := manifestArgs{imageName: imageName} tagRef, _ := reference.WithTag(imageName, tag) manifestURL, err := env.builder.BuildManifestURL(tagRef) if err != nil { t.Fatalf("unexpected error getting manifest url: %v", err) } // ----------------------------- // Attempt to fetch the manifest resp, err := http.Get(manifestURL) if err != nil { t.Fatalf("unexpected error getting manifest: %v", err) } defer resp.Body.Close() checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) tagsURL, err := env.builder.BuildTagsURL(imageName) if err != nil { t.Fatalf("unexpected error building tags url: %v", err) } resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() // Check that we get an unknown repository error when asking for tags checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) // -------------------------------- // Attempt to push unsigned manifest with missing layers unsignedManifest := &schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: imageName.Name(), Tag: tag, FSLayers: []schema1.FSLayer{ { BlobSum: "asdf", }, { BlobSum: "qwer", }, }, History: []schema1.History{ { V1Compatibility: "", }, { V1Compatibility: "", }, }, } resp = putManifest(t, "putting unsigned manifest", manifestURL, "", unsignedManifest) defer resp.Body.Close() checkResponse(t, "putting unsigned manifest", resp, http.StatusBadRequest) _, p, counts := checkBodyHasErrorCodes(t, "putting unsigned manifest", resp, v2.ErrorCodeManifestInvalid) expectedCounts := map[errcode.ErrorCode]int{ v2.ErrorCodeManifestInvalid: 1, } if !reflect.DeepEqual(counts, expectedCounts) { t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) } // sign the manifest and still get some interesting errors. sm, err := schema1.Sign(unsignedManifest, env.pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } resp = putManifest(t, "putting signed manifest with errors", manifestURL, "", sm) defer resp.Body.Close() checkResponse(t, "putting signed manifest with errors", resp, http.StatusBadRequest) _, p, counts = checkBodyHasErrorCodes(t, "putting signed manifest with errors", resp, v2.ErrorCodeManifestBlobUnknown, v2.ErrorCodeDigestInvalid) expectedCounts = map[errcode.ErrorCode]int{ v2.ErrorCodeManifestBlobUnknown: 2, v2.ErrorCodeDigestInvalid: 2, } if !reflect.DeepEqual(counts, expectedCounts) { t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) } // TODO(stevvooe): Add a test case where we take a mostly valid registry, // tamper with the content and ensure that we get an unverified manifest // error. // Push 2 random layers expectedLayers := make(map[digest.Digest]io.ReadSeeker) for i := range unsignedManifest.FSLayers { rs, dgstStr, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer %d: %v", i, err) } dgst := digest.Digest(dgstStr) expectedLayers[dgst] = rs unsignedManifest.FSLayers[i].BlobSum = dgst uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) } // ------------------- // Push the signed manifest with all layers pushed. signedManifest, err := schema1.Sign(unsignedManifest, env.pk) if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } dgst := digest.FromBytes(signedManifest.Canonical) args.manifest = signedManifest args.dgst = dgst digestRef, _ := reference.WithDigest(imageName, dgst) manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) checkErr(t, err, "building manifest url") resp = putManifest(t, "putting signed manifest no error", manifestURL, "", signedManifest) checkResponse(t, "putting signed manifest no error", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // -------------------- // Push by digest -- should get same result resp = putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) checkResponse(t, "putting signed manifest", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // ------------------ // Fetch by tag name resp, err = http.Get(manifestURL) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifest schema1.SignedManifest dec := json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } if !bytes.Equal(fetchedManifest.Canonical, signedManifest.Canonical) { t.Fatalf("manifests do not match") } // --------------- // Fetch by digest resp, err = http.Get(manifestDigestURL) checkErr(t, err, "fetching manifest by digest") defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifestByDigest schema1.SignedManifest dec = json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifestByDigest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } if !bytes.Equal(fetchedManifestByDigest.Canonical, signedManifest.Canonical) { t.Fatalf("manifests do not match") } // check signature was roundtripped signatures, err := fetchedManifestByDigest.Signatures() if err != nil { t.Fatal(err) } if len(signatures) != 1 { t.Fatalf("expected 1 signature from manifest, got: %d", len(signatures)) } // Re-sign, push and pull the same digest sm2, err := schema1.Sign(&fetchedManifestByDigest.Manifest, env.pk) if err != nil { t.Fatal(err) } // Re-push with a few different Content-Types. The official schema1 // content type should work, as should application/json with/without a // charset. resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, schema1.MediaTypeSignedManifest, sm2) checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json; charset=utf-8", sm2) checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) resp = putManifest(t, "re-putting signed manifest", manifestDigestURL, "application/json", sm2) checkResponse(t, "re-putting signed manifest", resp, http.StatusCreated) resp, err = http.Get(manifestDigestURL) checkErr(t, err, "re-fetching manifest by digest") defer resp.Body.Close() checkResponse(t, "re-fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) dec = json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifestByDigest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } // check only 1 signature is returned signatures, err = fetchedManifestByDigest.Signatures() if err != nil { t.Fatal(err) } if len(signatures) != 1 { t.Fatalf("expected 2 signature from manifest, got: %d", len(signatures)) } // Get by name with etag, gives 304 etag := resp.Header.Get("Etag") req, err := http.NewRequest("GET", manifestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) // Get by digest with etag, gives 304 req, err = http.NewRequest("GET", manifestDigestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) // Ensure that the tag is listed. resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() checkResponse(t, "getting tags", resp, http.StatusOK) dec = json.NewDecoder(resp.Body) var tagsResponse tagsAPIResponse if err := dec.Decode(&tagsResponse); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } if tagsResponse.Name != imageName.Name() { t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName.Name()) } if len(tagsResponse.Tags) != 1 { t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) } if tagsResponse.Tags[0] != tag { t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) } // Attempt to put a manifest with mismatching FSLayer and History array cardinalities unsignedManifest.History = append(unsignedManifest.History, schema1.History{ V1Compatibility: "", }) invalidSigned, err := schema1.Sign(unsignedManifest, env.pk) if err != nil { t.Fatalf("error signing manifest") } resp = putManifest(t, "putting invalid signed manifest", manifestDigestURL, "", invalidSigned) checkResponse(t, "putting invalid signed manifest", resp, http.StatusBadRequest) return args } func testManifestAPISchema2(t *testing.T, env *testEnv, imageName reference.Named) manifestArgs { tag := "schema2tag" args := manifestArgs{ imageName: imageName, mediaType: schema2.MediaTypeManifest, } tagRef, _ := reference.WithTag(imageName, tag) manifestURL, err := env.builder.BuildManifestURL(tagRef) if err != nil { t.Fatalf("unexpected error getting manifest url: %v", err) } // ----------------------------- // Attempt to fetch the manifest resp, err := http.Get(manifestURL) if err != nil { t.Fatalf("unexpected error getting manifest: %v", err) } defer resp.Body.Close() checkResponse(t, "getting non-existent manifest", resp, http.StatusNotFound) checkBodyHasErrorCodes(t, "getting non-existent manifest", resp, v2.ErrorCodeManifestUnknown) tagsURL, err := env.builder.BuildTagsURL(imageName) if err != nil { t.Fatalf("unexpected error building tags url: %v", err) } resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() // Check that we get an unknown repository error when asking for tags checkResponse(t, "getting unknown manifest tags", resp, http.StatusNotFound) checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeNameUnknown) // -------------------------------- // Attempt to push manifest with missing config and missing layers manifest := &schema2.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 2, MediaType: schema2.MediaTypeManifest, }, Config: distribution.Descriptor{ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Size: 3253, MediaType: schema2.MediaTypeConfig, }, Layers: []distribution.Descriptor{ { Digest: "sha256:463434349086340864309863409683460843608348608934092322395278926a", Size: 6323, MediaType: schema2.MediaTypeLayer, }, { Digest: "sha256:630923423623623423352523525237238023652897356239852383652aaaaaaa", Size: 6863, MediaType: schema2.MediaTypeLayer, }, }, } resp = putManifest(t, "putting missing config manifest", manifestURL, schema2.MediaTypeManifest, manifest) defer resp.Body.Close() checkResponse(t, "putting missing config manifest", resp, http.StatusBadRequest) _, p, counts := checkBodyHasErrorCodes(t, "putting missing config manifest", resp, v2.ErrorCodeManifestBlobUnknown) expectedCounts := map[errcode.ErrorCode]int{ v2.ErrorCodeManifestBlobUnknown: 3, } if !reflect.DeepEqual(counts, expectedCounts) { t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) } // Push a config, and reference it in the manifest sampleConfig := []byte(`{ "architecture": "amd64", "history": [ { "created": "2015-10-31T22:22:54.690851953Z", "created_by": "/bin/sh -c #(nop) ADD file:a3bc1e842b69636f9df5256c49c5374fb4eef1e281fe3f282c65fb853ee171c5 in /" }, { "created": "2015-10-31T22:22:55.613815829Z", "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]" } ], "rootfs": { "diff_ids": [ "sha256:c6f988f4874bb0add23a778f753c65efe992244e148a1d2ec2a8b664fb66bbd1", "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef" ], "type": "layers" } }`) sampleConfigDigest := digest.FromBytes(sampleConfig) uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, sampleConfigDigest, uploadURLBase, bytes.NewReader(sampleConfig)) manifest.Config.Digest = sampleConfigDigest manifest.Config.Size = int64(len(sampleConfig)) // The manifest should still be invalid, because its layer doesn't exist resp = putManifest(t, "putting missing layer manifest", manifestURL, schema2.MediaTypeManifest, manifest) defer resp.Body.Close() checkResponse(t, "putting missing layer manifest", resp, http.StatusBadRequest) _, p, counts = checkBodyHasErrorCodes(t, "getting unknown manifest tags", resp, v2.ErrorCodeManifestBlobUnknown) expectedCounts = map[errcode.ErrorCode]int{ v2.ErrorCodeManifestBlobUnknown: 2, } if !reflect.DeepEqual(counts, expectedCounts) { t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) } // Push 2 random layers expectedLayers := make(map[digest.Digest]io.ReadSeeker) for i := range manifest.Layers { rs, dgstStr, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer %d: %v", i, err) } dgst := digest.Digest(dgstStr) expectedLayers[dgst] = rs manifest.Layers[i].Digest = dgst uploadURLBase, _ := startPushLayer(t, env, imageName) pushLayer(t, env.builder, imageName, dgst, uploadURLBase, rs) } // ------------------- // Push the manifest with all layers pushed. deserializedManifest, err := schema2.FromStruct(*manifest) if err != nil { t.Fatalf("could not create DeserializedManifest: %v", err) } _, canonical, err := deserializedManifest.Payload() if err != nil { t.Fatalf("could not get manifest payload: %v", err) } dgst := digest.FromBytes(canonical) args.dgst = dgst args.manifest = deserializedManifest digestRef, _ := reference.WithDigest(imageName, dgst) manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) checkErr(t, err, "building manifest url") resp = putManifest(t, "putting manifest no error", manifestURL, schema2.MediaTypeManifest, manifest) checkResponse(t, "putting manifest no error", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // -------------------- // Push by digest -- should get same result resp = putManifest(t, "putting manifest by digest", manifestDigestURL, schema2.MediaTypeManifest, manifest) checkResponse(t, "putting manifest by digest", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // ------------------ // Fetch by tag name req, err := http.NewRequest("GET", manifestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("Accept", schema2.MediaTypeManifest) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifest schema2.DeserializedManifest dec := json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } _, fetchedCanonical, err := fetchedManifest.Payload() if err != nil { t.Fatalf("error getting manifest payload: %v", err) } if !bytes.Equal(fetchedCanonical, canonical) { t.Fatalf("manifests do not match") } // --------------- // Fetch by digest req, err = http.NewRequest("GET", manifestDigestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("Accept", schema2.MediaTypeManifest) resp, err = http.DefaultClient.Do(req) checkErr(t, err, "fetching manifest by digest") defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifestByDigest schema2.DeserializedManifest dec = json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifestByDigest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } _, fetchedCanonical, err = fetchedManifest.Payload() if err != nil { t.Fatalf("error getting manifest payload: %v", err) } if !bytes.Equal(fetchedCanonical, canonical) { t.Fatalf("manifests do not match") } // Get by name with etag, gives 304 etag := resp.Header.Get("Etag") req, err = http.NewRequest("GET", manifestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) // Get by digest with etag, gives 304 req, err = http.NewRequest("GET", manifestDigestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) // Ensure that the tag is listed. resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() checkResponse(t, "getting unknown manifest tags", resp, http.StatusOK) dec = json.NewDecoder(resp.Body) var tagsResponse tagsAPIResponse if err := dec.Decode(&tagsResponse); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } if tagsResponse.Name != imageName.Name() { t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) } if len(tagsResponse.Tags) != 1 { t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) } if tagsResponse.Tags[0] != tag { t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) } // ------------------ // Fetch as a schema1 manifest resp, err = http.Get(manifestURL) if err != nil { t.Fatalf("unexpected error fetching manifest as schema1: %v", err) } defer resp.Body.Close() manifestBytes, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("error reading response body: %v", err) } checkResponse(t, "fetching uploaded manifest as schema1", resp, http.StatusOK) m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) if err != nil { t.Fatalf("unexpected error unmarshalling manifest: %v", err) } fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) if !ok { t.Fatalf("expecting schema1 manifest") } checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{desc.Digest.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, }) if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { t.Fatal("wrong schema version") } if fetchedSchema1Manifest.Architecture != "amd64" { t.Fatal("wrong architecture") } if fetchedSchema1Manifest.Name != imageName.Name() { t.Fatal("wrong image name") } if fetchedSchema1Manifest.Tag != tag { t.Fatal("wrong tag") } if len(fetchedSchema1Manifest.FSLayers) != 2 { t.Fatal("wrong number of FSLayers") } for i := range manifest.Layers { if fetchedSchema1Manifest.FSLayers[i].BlobSum != manifest.Layers[len(manifest.Layers)-i-1].Digest { t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) } } if len(fetchedSchema1Manifest.History) != 2 { t.Fatal("wrong number of History entries") } // Don't check V1Compatibility fields because we're using randomly-generated // layers. return args } func testManifestAPIManifestList(t *testing.T, env *testEnv, args manifestArgs) { imageName := args.imageName tag := "manifestlisttag" tagRef, _ := reference.WithTag(imageName, tag) manifestURL, err := env.builder.BuildManifestURL(tagRef) if err != nil { t.Fatalf("unexpected error getting manifest url: %v", err) } // -------------------------------- // Attempt to push manifest list that refers to an unknown manifest manifestList := &manifestlist.ManifestList{ Versioned: manifest.Versioned{ SchemaVersion: 2, MediaType: manifestlist.MediaTypeManifestList, }, Manifests: []manifestlist.ManifestDescriptor{ { Descriptor: distribution.Descriptor{ Digest: "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", Size: 3253, MediaType: schema2.MediaTypeManifest, }, Platform: manifestlist.PlatformSpec{ Architecture: "amd64", OS: "linux", }, }, }, } resp := putManifest(t, "putting missing manifest manifestlist", manifestURL, manifestlist.MediaTypeManifestList, manifestList) defer resp.Body.Close() checkResponse(t, "putting missing manifest manifestlist", resp, http.StatusBadRequest) _, p, counts := checkBodyHasErrorCodes(t, "putting missing manifest manifestlist", resp, v2.ErrorCodeManifestBlobUnknown) expectedCounts := map[errcode.ErrorCode]int{ v2.ErrorCodeManifestBlobUnknown: 1, } if !reflect.DeepEqual(counts, expectedCounts) { t.Fatalf("unexpected number of error codes encountered: %v\n!=\n%v\n---\n%s", counts, expectedCounts, string(p)) } // ------------------- // Push a manifest list that references an actual manifest manifestList.Manifests[0].Digest = args.dgst deserializedManifestList, err := manifestlist.FromDescriptors(manifestList.Manifests) if err != nil { t.Fatalf("could not create DeserializedManifestList: %v", err) } _, canonical, err := deserializedManifestList.Payload() if err != nil { t.Fatalf("could not get manifest list payload: %v", err) } dgst := digest.FromBytes(canonical) digestRef, _ := reference.WithDigest(imageName, dgst) manifestDigestURL, err := env.builder.BuildManifestURL(digestRef) checkErr(t, err, "building manifest url") resp = putManifest(t, "putting manifest list no error", manifestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) checkResponse(t, "putting manifest list no error", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // -------------------- // Push by digest -- should get same result resp = putManifest(t, "putting manifest list by digest", manifestDigestURL, manifestlist.MediaTypeManifestList, deserializedManifestList) checkResponse(t, "putting manifest list by digest", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // ------------------ // Fetch by tag name req, err := http.NewRequest("GET", manifestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } // multiple headers in mixed list format to ensure we parse correctly server-side req.Header.Set("Accept", fmt.Sprintf(` %s ; q=0.8 , %s ; q=0.5 `, manifestlist.MediaTypeManifestList, schema1.MediaTypeSignedManifest)) req.Header.Add("Accept", schema2.MediaTypeManifest) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("unexpected error fetching manifest list: %v", err) } defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifestList manifestlist.DeserializedManifestList dec := json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifestList); err != nil { t.Fatalf("error decoding fetched manifest list: %v", err) } _, fetchedCanonical, err := fetchedManifestList.Payload() if err != nil { t.Fatalf("error getting manifest list payload: %v", err) } if !bytes.Equal(fetchedCanonical, canonical) { t.Fatalf("manifest lists do not match") } // --------------- // Fetch by digest req, err = http.NewRequest("GET", manifestDigestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("Accept", manifestlist.MediaTypeManifestList) resp, err = http.DefaultClient.Do(req) checkErr(t, err, "fetching manifest list by digest") defer resp.Body.Close() checkResponse(t, "fetching uploaded manifest list", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, dgst)}, }) var fetchedManifestListByDigest manifestlist.DeserializedManifestList dec = json.NewDecoder(resp.Body) if err := dec.Decode(&fetchedManifestListByDigest); err != nil { t.Fatalf("error decoding fetched manifest: %v", err) } _, fetchedCanonical, err = fetchedManifestListByDigest.Payload() if err != nil { t.Fatalf("error getting manifest list payload: %v", err) } if !bytes.Equal(fetchedCanonical, canonical) { t.Fatalf("manifests do not match") } // Get by name with etag, gives 304 etag := resp.Header.Get("Etag") req, err = http.NewRequest("GET", manifestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by name with etag", resp, http.StatusNotModified) // Get by digest with etag, gives 304 req, err = http.NewRequest("GET", manifestDigestURL, nil) if err != nil { t.Fatalf("Error constructing request: %s", err) } req.Header.Set("If-None-Match", etag) resp, err = http.DefaultClient.Do(req) if err != nil { t.Fatalf("Error constructing request: %s", err) } checkResponse(t, "fetching manifest by dgst with etag", resp, http.StatusNotModified) // ------------------ // Fetch as a schema1 manifest resp, err = http.Get(manifestURL) if err != nil { t.Fatalf("unexpected error fetching manifest list as schema1: %v", err) } defer resp.Body.Close() manifestBytes, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("error reading response body: %v", err) } checkResponse(t, "fetching uploaded manifest list as schema1", resp, http.StatusOK) m, desc, err := distribution.UnmarshalManifest(schema1.MediaTypeManifest, manifestBytes) if err != nil { t.Fatalf("unexpected error unmarshalling manifest: %v", err) } fetchedSchema1Manifest, ok := m.(*schema1.SignedManifest) if !ok { t.Fatalf("expecting schema1 manifest") } checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{desc.Digest.String()}, "ETag": []string{fmt.Sprintf(`"%s"`, desc.Digest)}, }) if fetchedSchema1Manifest.Manifest.SchemaVersion != 1 { t.Fatal("wrong schema version") } if fetchedSchema1Manifest.Architecture != "amd64" { t.Fatal("wrong architecture") } if fetchedSchema1Manifest.Name != imageName.Name() { t.Fatal("wrong image name") } if fetchedSchema1Manifest.Tag != tag { t.Fatal("wrong tag") } if len(fetchedSchema1Manifest.FSLayers) != 2 { t.Fatal("wrong number of FSLayers") } layers := args.manifest.(*schema2.DeserializedManifest).Layers for i := range layers { if fetchedSchema1Manifest.FSLayers[i].BlobSum != layers[len(layers)-i-1].Digest { t.Fatalf("blob digest mismatch in schema1 manifest for layer %d", i) } } if len(fetchedSchema1Manifest.History) != 2 { t.Fatal("wrong number of History entries") } // Don't check V1Compatibility fields because we're using randomly-generated // layers. } func testManifestDelete(t *testing.T, env *testEnv, args manifestArgs) { imageName := args.imageName dgst := args.dgst manifest := args.manifest ref, _ := reference.WithDigest(imageName, dgst) manifestDigestURL, err := env.builder.BuildManifestURL(ref) // --------------- // Delete by digest resp, err := httpDelete(manifestDigestURL) checkErr(t, err, "deleting manifest by digest") checkResponse(t, "deleting manifest", resp, http.StatusAccepted) checkHeaders(t, resp, http.Header{ "Content-Length": []string{"0"}, }) // --------------- // Attempt to fetch deleted manifest resp, err = http.Get(manifestDigestURL) checkErr(t, err, "fetching deleted manifest by digest") defer resp.Body.Close() checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) // --------------- // Delete already deleted manifest by digest resp, err = httpDelete(manifestDigestURL) checkErr(t, err, "re-deleting manifest by digest") checkResponse(t, "re-deleting manifest", resp, http.StatusNotFound) // -------------------- // Re-upload manifest by digest resp = putManifest(t, "putting manifest", manifestDigestURL, args.mediaType, manifest) checkResponse(t, "putting manifest", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) // --------------- // Attempt to fetch re-uploaded deleted digest resp, err = http.Get(manifestDigestURL) checkErr(t, err, "fetching re-uploaded manifest by digest") defer resp.Body.Close() checkResponse(t, "fetching re-uploaded manifest", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, }) // --------------- // Attempt to delete an unknown manifest unknownDigest := digest.Digest("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") unknownRef, _ := reference.WithDigest(imageName, unknownDigest) unknownManifestDigestURL, err := env.builder.BuildManifestURL(unknownRef) checkErr(t, err, "building unknown manifest url") resp, err = httpDelete(unknownManifestDigestURL) checkErr(t, err, "delting unknown manifest by digest") checkResponse(t, "fetching deleted manifest", resp, http.StatusNotFound) // -------------------- // Upload manifest by tag tag := "atag" tagRef, _ := reference.WithTag(imageName, tag) manifestTagURL, err := env.builder.BuildManifestURL(tagRef) resp = putManifest(t, "putting manifest by tag", manifestTagURL, args.mediaType, manifest) checkResponse(t, "putting manifest by tag", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{manifestDigestURL}, "Docker-Content-Digest": []string{dgst.String()}, }) tagsURL, err := env.builder.BuildTagsURL(imageName) if err != nil { t.Fatalf("unexpected error building tags url: %v", err) } // Ensure that the tag is listed. resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() dec := json.NewDecoder(resp.Body) var tagsResponse tagsAPIResponse if err := dec.Decode(&tagsResponse); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } if tagsResponse.Name != imageName.Name() { t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) } if len(tagsResponse.Tags) != 1 { t.Fatalf("expected some tags in response: %v", tagsResponse.Tags) } if tagsResponse.Tags[0] != tag { t.Fatalf("tag not as expected: %q != %q", tagsResponse.Tags[0], tag) } // --------------- // Delete by digest resp, err = httpDelete(manifestDigestURL) checkErr(t, err, "deleting manifest by digest") checkResponse(t, "deleting manifest with tag", resp, http.StatusAccepted) checkHeaders(t, resp, http.Header{ "Content-Length": []string{"0"}, }) // Ensure that the tag is not listed. resp, err = http.Get(tagsURL) if err != nil { t.Fatalf("unexpected error getting unknown tags: %v", err) } defer resp.Body.Close() dec = json.NewDecoder(resp.Body) if err := dec.Decode(&tagsResponse); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } if tagsResponse.Name != imageName.Name() { t.Fatalf("tags name should match image name: %v != %v", tagsResponse.Name, imageName) } if len(tagsResponse.Tags) != 0 { t.Fatalf("expected 0 tags in response: %v", tagsResponse.Tags) } } type testEnv struct { pk libtrust.PrivateKey ctx context.Context config configuration.Configuration app *App server *httptest.Server builder *v2.URLBuilder } func newTestEnvMirror(t *testing.T, deleteEnabled bool) *testEnv { config := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, "delete": configuration.Parameters{"enabled": deleteEnabled}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, Proxy: configuration.Proxy{ RemoteURL: "http://example.com", }, } return newTestEnvWithConfig(t, &config) } func newTestEnv(t *testing.T, deleteEnabled bool) *testEnv { config := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, "delete": configuration.Parameters{"enabled": deleteEnabled}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, } config.HTTP.Headers = headerConfig return newTestEnvWithConfig(t, &config) } func newTestEnvWithConfig(t *testing.T, config *configuration.Configuration) *testEnv { ctx := context.Background() app := NewApp(ctx, config) server := httptest.NewServer(handlers.CombinedLoggingHandler(os.Stderr, app)) builder, err := v2.NewURLBuilderFromString(server.URL+config.HTTP.Prefix, false) if err != nil { t.Fatalf("error creating url builder: %v", err) } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } return &testEnv{ pk: pk, ctx: ctx, config: *config, app: app, server: server, builder: builder, } } func (t *testEnv) Shutdown() { t.server.CloseClientConnections() t.server.Close() } func putManifest(t *testing.T, msg, url, contentType string, v interface{}) *http.Response { var body []byte switch m := v.(type) { case *schema1.SignedManifest: _, pl, err := m.Payload() if err != nil { t.Fatalf("error getting payload: %v", err) } body = pl case *manifestlist.DeserializedManifestList: _, pl, err := m.Payload() if err != nil { t.Fatalf("error getting payload: %v", err) } body = pl default: var err error body, err = json.MarshalIndent(v, "", " ") if err != nil { t.Fatalf("unexpected error marshaling %v: %v", v, err) } } req, err := http.NewRequest("PUT", url, bytes.NewReader(body)) if err != nil { t.Fatalf("error creating request for %s: %v", msg, err) } if contentType != "" { req.Header.Set("Content-Type", contentType) } resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatalf("error doing put request while %s: %v", msg, err) } return resp } func startPushLayer(t *testing.T, env *testEnv, name reference.Named) (location string, uuid string) { layerUploadURL, err := env.builder.BuildBlobUploadURL(name) if err != nil { t.Fatalf("unexpected error building layer upload url: %v", err) } u, err := url.Parse(layerUploadURL) if err != nil { t.Fatalf("error parsing layer upload URL: %v", err) } base, err := url.Parse(env.server.URL) if err != nil { t.Fatalf("error parsing server URL: %v", err) } layerUploadURL = base.ResolveReference(u).String() resp, err := http.Post(layerUploadURL, "", nil) if err != nil { t.Fatalf("unexpected error starting layer push: %v", err) } defer resp.Body.Close() checkResponse(t, fmt.Sprintf("pushing starting layer push %v", name.String()), resp, http.StatusAccepted) u, err = url.Parse(resp.Header.Get("Location")) if err != nil { t.Fatalf("error parsing location header: %v", err) } uuid = path.Base(u.Path) checkHeaders(t, resp, http.Header{ "Location": []string{"*"}, "Content-Length": []string{"0"}, "Docker-Upload-UUID": []string{uuid}, }) return resp.Header.Get("Location"), uuid } // doPushLayer pushes the layer content returning the url on success returning // the response. If you're only expecting a successful response, use pushLayer. func doPushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) (*http.Response, error) { u, err := url.Parse(uploadURLBase) if err != nil { t.Fatalf("unexpected error parsing pushLayer url: %v", err) } u.RawQuery = url.Values{ "_state": u.Query()["_state"], "digest": []string{dgst.String()}, }.Encode() uploadURL := u.String() // Just do a monolithic upload req, err := http.NewRequest("PUT", uploadURL, body) if err != nil { t.Fatalf("unexpected error creating new request: %v", err) } return http.DefaultClient.Do(req) } // pushLayer pushes the layer content returning the url on success. func pushLayer(t *testing.T, ub *v2.URLBuilder, name reference.Named, dgst digest.Digest, uploadURLBase string, body io.Reader) string { digester := digest.Canonical.New() resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, io.TeeReader(body, digester.Hash())) if err != nil { t.Fatalf("unexpected error doing push layer request: %v", err) } defer resp.Body.Close() checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) if err != nil { t.Fatalf("error generating sha256 digest of body") } sha256Dgst := digester.Digest() ref, _ := reference.WithDigest(name, sha256Dgst) expectedLayerURL, err := ub.BuildBlobURL(ref) if err != nil { t.Fatalf("error building expected layer url: %v", err) } checkHeaders(t, resp, http.Header{ "Location": []string{expectedLayerURL}, "Content-Length": []string{"0"}, "Docker-Content-Digest": []string{sha256Dgst.String()}, }) return resp.Header.Get("Location") } func finishUpload(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, dgst digest.Digest) string { resp, err := doPushLayer(t, ub, name, dgst, uploadURLBase, nil) if err != nil { t.Fatalf("unexpected error doing push layer request: %v", err) } defer resp.Body.Close() checkResponse(t, "putting monolithic chunk", resp, http.StatusCreated) ref, _ := reference.WithDigest(name, dgst) expectedLayerURL, err := ub.BuildBlobURL(ref) if err != nil { t.Fatalf("error building expected layer url: %v", err) } checkHeaders(t, resp, http.Header{ "Location": []string{expectedLayerURL}, "Content-Length": []string{"0"}, "Docker-Content-Digest": []string{dgst.String()}, }) return resp.Header.Get("Location") } func doPushChunk(t *testing.T, uploadURLBase string, body io.Reader) (*http.Response, digest.Digest, error) { u, err := url.Parse(uploadURLBase) if err != nil { t.Fatalf("unexpected error parsing pushLayer url: %v", err) } u.RawQuery = url.Values{ "_state": u.Query()["_state"], }.Encode() uploadURL := u.String() digester := digest.Canonical.New() req, err := http.NewRequest("PATCH", uploadURL, io.TeeReader(body, digester.Hash())) if err != nil { t.Fatalf("unexpected error creating new request: %v", err) } req.Header.Set("Content-Type", "application/octet-stream") resp, err := http.DefaultClient.Do(req) return resp, digester.Digest(), err } func pushChunk(t *testing.T, ub *v2.URLBuilder, name reference.Named, uploadURLBase string, body io.Reader, length int64) (string, digest.Digest) { resp, dgst, err := doPushChunk(t, uploadURLBase, body) if err != nil { t.Fatalf("unexpected error doing push layer request: %v", err) } defer resp.Body.Close() checkResponse(t, "putting chunk", resp, http.StatusAccepted) if err != nil { t.Fatalf("error generating sha256 digest of body") } checkHeaders(t, resp, http.Header{ "Range": []string{fmt.Sprintf("0-%d", length-1)}, "Content-Length": []string{"0"}, }) return resp.Header.Get("Location"), dgst } func checkResponse(t *testing.T, msg string, resp *http.Response, expectedStatus int) { if resp.StatusCode != expectedStatus { t.Logf("unexpected status %s: %v != %v", msg, resp.StatusCode, expectedStatus) maybeDumpResponse(t, resp) t.FailNow() } // We expect the headers included in the configuration, unless the // status code is 405 (Method Not Allowed), which means the handler // doesn't even get called. if resp.StatusCode != 405 && !reflect.DeepEqual(resp.Header["X-Content-Type-Options"], []string{"nosniff"}) { t.Logf("missing or incorrect header X-Content-Type-Options %s", msg) maybeDumpResponse(t, resp) t.FailNow() } } // checkBodyHasErrorCodes ensures the body is an error body and has the // expected error codes, returning the error structure, the json slice and a // count of the errors by code. func checkBodyHasErrorCodes(t *testing.T, msg string, resp *http.Response, errorCodes ...errcode.ErrorCode) (errcode.Errors, []byte, map[errcode.ErrorCode]int) { p, err := ioutil.ReadAll(resp.Body) if err != nil { t.Fatalf("unexpected error reading body %s: %v", msg, err) } var errs errcode.Errors if err := json.Unmarshal(p, &errs); err != nil { t.Fatalf("unexpected error decoding error response: %v", err) } if len(errs) == 0 { t.Fatalf("expected errors in response") } // TODO(stevvooe): Shoot. The error setup is not working out. The content- // type headers are being set after writing the status code. // if resp.Header.Get("Content-Type") != "application/json; charset=utf-8" { // t.Fatalf("unexpected content type: %v != 'application/json'", // resp.Header.Get("Content-Type")) // } expected := map[errcode.ErrorCode]struct{}{} counts := map[errcode.ErrorCode]int{} // Initialize map with zeros for expected for _, code := range errorCodes { expected[code] = struct{}{} counts[code] = 0 } for _, e := range errs { err, ok := e.(errcode.ErrorCoder) if !ok { t.Fatalf("not an ErrorCoder: %#v", e) } if _, ok := expected[err.ErrorCode()]; !ok { t.Fatalf("unexpected error code %v encountered during %s: %s ", err.ErrorCode(), msg, string(p)) } counts[err.ErrorCode()]++ } // Ensure that counts of expected errors were all non-zero for code := range expected { if counts[code] == 0 { t.Fatalf("expected error code %v not encounterd during %s: %s", code, msg, string(p)) } } return errs, p, counts } func maybeDumpResponse(t *testing.T, resp *http.Response) { if d, err := httputil.DumpResponse(resp, true); err != nil { t.Logf("error dumping response: %v", err) } else { t.Logf("response:\n%s", string(d)) } } // matchHeaders checks that the response has at least the headers. If not, the // test will fail. If a passed in header value is "*", any non-zero value will // suffice as a match. func checkHeaders(t *testing.T, resp *http.Response, headers http.Header) { for k, vs := range headers { if resp.Header.Get(k) == "" { t.Fatalf("response missing header %q", k) } for _, v := range vs { if v == "*" { // Just ensure there is some value. if len(resp.Header[http.CanonicalHeaderKey(k)]) > 0 { continue } } for _, hv := range resp.Header[http.CanonicalHeaderKey(k)] { if hv != v { t.Fatalf("%+v %v header value not matched in response: %q != %q", resp.Header, k, hv, v) } } } } } func checkErr(t *testing.T, err error, msg string) { if err != nil { t.Fatalf("unexpected error %s: %v", msg, err) } } func createRepository(env *testEnv, t *testing.T, imageName string, tag string) digest.Digest { imageNameRef, err := reference.ParseNamed(imageName) if err != nil { t.Fatalf("unable to parse reference: %v", err) } unsignedManifest := &schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: imageName, Tag: tag, FSLayers: []schema1.FSLayer{ { BlobSum: "asdf", }, }, History: []schema1.History{ { V1Compatibility: "", }, }, } // Push 2 random layers expectedLayers := make(map[digest.Digest]io.ReadSeeker) for i := range unsignedManifest.FSLayers { rs, dgstStr, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random layer %d: %v", i, err) } dgst := digest.Digest(dgstStr) expectedLayers[dgst] = rs unsignedManifest.FSLayers[i].BlobSum = dgst uploadURLBase, _ := startPushLayer(t, env, imageNameRef) pushLayer(t, env.builder, imageNameRef, dgst, uploadURLBase, rs) } signedManifest, err := schema1.Sign(unsignedManifest, env.pk) if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } dgst := digest.FromBytes(signedManifest.Canonical) // Create this repository by tag to ensure the tag mapping is made in the registry tagRef, _ := reference.WithTag(imageNameRef, tag) manifestDigestURL, err := env.builder.BuildManifestURL(tagRef) checkErr(t, err, "building manifest url") digestRef, _ := reference.WithDigest(imageNameRef, dgst) location, err := env.builder.BuildManifestURL(digestRef) checkErr(t, err, "building location URL") resp := putManifest(t, "putting signed manifest", manifestDigestURL, "", signedManifest) checkResponse(t, "putting signed manifest", resp, http.StatusCreated) checkHeaders(t, resp, http.Header{ "Location": []string{location}, "Docker-Content-Digest": []string{dgst.String()}, }) return dgst } // Test mutation operations on a registry configured as a cache. Ensure that they return // appropriate errors. func TestRegistryAsCacheMutationAPIs(t *testing.T) { deleteEnabled := true env := newTestEnvMirror(t, deleteEnabled) defer env.Shutdown() imageName, _ := reference.ParseNamed("foo/bar") tag := "latest" tagRef, _ := reference.WithTag(imageName, tag) manifestURL, err := env.builder.BuildManifestURL(tagRef) if err != nil { t.Fatalf("unexpected error building base url: %v", err) } // Manifest upload m := &schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: imageName.Name(), Tag: tag, FSLayers: []schema1.FSLayer{}, History: []schema1.History{}, } sm, err := schema1.Sign(m, env.pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } resp := putManifest(t, "putting unsigned manifest", manifestURL, "", sm) checkResponse(t, "putting signed manifest to cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) // Manifest Delete resp, err = httpDelete(manifestURL) checkResponse(t, "deleting signed manifest from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) // Blob upload initialization layerUploadURL, err := env.builder.BuildBlobUploadURL(imageName) if err != nil { t.Fatalf("unexpected error building layer upload url: %v", err) } resp, err = http.Post(layerUploadURL, "", nil) if err != nil { t.Fatalf("unexpected error starting layer push: %v", err) } defer resp.Body.Close() checkResponse(t, fmt.Sprintf("starting layer push to cache %v", imageName), resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) // Blob Delete ref, _ := reference.WithDigest(imageName, digest.DigestSha256EmptyTar) blobURL, err := env.builder.BuildBlobURL(ref) resp, err = httpDelete(blobURL) checkResponse(t, "deleting blob from cache", resp, errcode.ErrorCodeUnsupported.Descriptor().HTTPStatusCode) } // TestCheckContextNotifier makes sure the API endpoints get a ResponseWriter // that implements http.ContextNotifier. func TestCheckContextNotifier(t *testing.T) { env := newTestEnv(t, false) defer env.Shutdown() // Register a new endpoint for testing env.app.router.Handle("/unittest/{name}/", env.app.dispatcher(func(ctx *Context, r *http.Request) http.Handler { return handlers.MethodHandler{ "GET": http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if _, ok := w.(http.CloseNotifier); !ok { t.Fatal("could not cast ResponseWriter to CloseNotifier") } w.WriteHeader(200) }), } })) resp, err := http.Get(env.server.URL + "/unittest/reponame/") if err != nil { t.Fatalf("unexpected error issuing request: %v", err) } defer resp.Body.Close() if resp.StatusCode != 200 { t.Fatalf("wrong status code - expected 200, got %d", resp.StatusCode) } } func TestProxyManifestGetByTag(t *testing.T) { truthConfig := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, } truthConfig.HTTP.Headers = headerConfig imageName, _ := reference.ParseNamed("foo/bar") tag := "latest" truthEnv := newTestEnvWithConfig(t, &truthConfig) defer truthEnv.Shutdown() // create a repository in the truth registry dgst := createRepository(truthEnv, t, imageName.Name(), tag) proxyConfig := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": configuration.Parameters{}, }, Proxy: configuration.Proxy{ RemoteURL: truthEnv.server.URL, }, } proxyConfig.HTTP.Headers = headerConfig proxyEnv := newTestEnvWithConfig(t, &proxyConfig) defer proxyEnv.Shutdown() digestRef, _ := reference.WithDigest(imageName, dgst) manifestDigestURL, err := proxyEnv.builder.BuildManifestURL(digestRef) checkErr(t, err, "building manifest url") resp, err := http.Get(manifestDigestURL) checkErr(t, err, "fetching manifest from proxy by digest") defer resp.Body.Close() tagRef, _ := reference.WithTag(imageName, tag) manifestTagURL, err := proxyEnv.builder.BuildManifestURL(tagRef) checkErr(t, err, "building manifest url") resp, err = http.Get(manifestTagURL) checkErr(t, err, "fetching manifest from proxy by tag") defer resp.Body.Close() checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{dgst.String()}, }) // Create another manifest in the remote with the same image/tag pair newDigest := createRepository(truthEnv, t, imageName.Name(), tag) if dgst == newDigest { t.Fatalf("non-random test data") } // fetch it with the same proxy URL as before. Ensure the updated content is at the same tag resp, err = http.Get(manifestTagURL) checkErr(t, err, "fetching manifest from proxy by tag") defer resp.Body.Close() checkResponse(t, "fetching manifest from proxy by tag", resp, http.StatusOK) checkHeaders(t, resp, http.Header{ "Docker-Content-Digest": []string{newDigest.String()}, }) } docker-registry-2.6.2~ds1/registry/handlers/app.go000066400000000000000000001053741313450123100222260ustar00rootroot00000000000000package handlers import ( cryptorand "crypto/rand" "expvar" "fmt" "math/rand" "net" "net/http" "net/url" "os" "regexp" "runtime" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/docker/distribution" "github.com/docker/distribution/configuration" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/health" "github.com/docker/distribution/health/checks" "github.com/docker/distribution/notifications" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/auth" registrymiddleware "github.com/docker/distribution/registry/middleware/registry" repositorymiddleware "github.com/docker/distribution/registry/middleware/repository" "github.com/docker/distribution/registry/proxy" "github.com/docker/distribution/registry/storage" memorycache "github.com/docker/distribution/registry/storage/cache/memory" rediscache "github.com/docker/distribution/registry/storage/cache/redis" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/factory" storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" "github.com/docker/distribution/version" "github.com/docker/libtrust" "github.com/garyburd/redigo/redis" "github.com/gorilla/mux" "golang.org/x/net/context" ) // randomSecretSize is the number of random bytes to generate if no secret // was specified. const randomSecretSize = 32 // defaultCheckInterval is the default time in between health checks const defaultCheckInterval = 10 * time.Second // App is a global registry application object. Shared resources can be placed // on this object that will be accessible from all requests. Any writable // fields should be protected. type App struct { context.Context Config *configuration.Configuration router *mux.Router // main application router, configured with dispatchers driver storagedriver.StorageDriver // driver maintains the app global storage driver instance. registry distribution.Namespace // registry is the primary registry backend for the app instance. accessController auth.AccessController // main access controller for application // httpHost is a parsed representation of the http.host parameter from // the configuration. Only the Scheme and Host fields are used. httpHost url.URL // events contains notification related configuration. events struct { sink notifications.Sink source notifications.SourceRecord } redis *redis.Pool // trustKey is a deprecated key used to sign manifests converted to // schema1 for backward compatibility. It should not be used for any // other purposes. trustKey libtrust.PrivateKey // isCache is true if this registry is configured as a pull through cache isCache bool // readOnly is true if the registry is in a read-only maintenance mode readOnly bool } // NewApp takes a configuration and returns a configured app, ready to serve // requests. The app only implements ServeHTTP and can be wrapped in other // handlers accordingly. func NewApp(ctx context.Context, config *configuration.Configuration) *App { app := &App{ Config: config, Context: ctx, router: v2.RouterWithPrefix(config.HTTP.Prefix), isCache: config.Proxy.RemoteURL != "", } // Register the handler dispatchers. app.register(v2.RouteNameBase, func(ctx *Context, r *http.Request) http.Handler { return http.HandlerFunc(apiBase) }) app.register(v2.RouteNameManifest, imageManifestDispatcher) app.register(v2.RouteNameCatalog, catalogDispatcher) app.register(v2.RouteNameTags, tagsDispatcher) app.register(v2.RouteNameBlob, blobDispatcher) app.register(v2.RouteNameBlobUpload, blobUploadDispatcher) app.register(v2.RouteNameBlobUploadChunk, blobUploadDispatcher) // override the storage driver's UA string for registry outbound HTTP requests storageParams := config.Storage.Parameters() if storageParams == nil { storageParams = make(configuration.Parameters) } storageParams["useragent"] = fmt.Sprintf("docker-distribution/%s %s", version.Version, runtime.Version()) var err error app.driver, err = factory.Create(config.Storage.Type(), storageParams) if err != nil { // TODO(stevvooe): Move the creation of a service into a protected // method, where this is created lazily. Its status can be queried via // a health check. panic(err) } purgeConfig := uploadPurgeDefaultConfig() if mc, ok := config.Storage["maintenance"]; ok { if v, ok := mc["uploadpurging"]; ok { purgeConfig, ok = v.(map[interface{}]interface{}) if !ok { panic("uploadpurging config key must contain additional keys") } } if v, ok := mc["readonly"]; ok { readOnly, ok := v.(map[interface{}]interface{}) if !ok { panic("readonly config key must contain additional keys") } if readOnlyEnabled, ok := readOnly["enabled"]; ok { app.readOnly, ok = readOnlyEnabled.(bool) if !ok { panic("readonly's enabled config key must have a boolean value") } } } } startUploadPurger(app, app.driver, ctxu.GetLogger(app), purgeConfig) app.driver, err = applyStorageMiddleware(app.driver, config.Middleware["storage"]) if err != nil { panic(err) } app.configureSecret(config) app.configureEvents(config) app.configureRedis(config) app.configureLogHook(config) options := registrymiddleware.GetRegistryOptions() if config.Compatibility.Schema1.TrustKey != "" { app.trustKey, err = libtrust.LoadKeyFile(config.Compatibility.Schema1.TrustKey) if err != nil { panic(fmt.Sprintf(`could not load schema1 "signingkey" parameter: %v`, err)) } } else { // Generate an ephemeral key to be used for signing converted manifests // for clients that don't support schema2. app.trustKey, err = libtrust.GenerateECP256PrivateKey() if err != nil { panic(err) } } options = append(options, storage.Schema1SigningKey(app.trustKey)) if config.HTTP.Host != "" { u, err := url.Parse(config.HTTP.Host) if err != nil { panic(fmt.Sprintf(`could not parse http "host" parameter: %v`, err)) } app.httpHost = *u } if app.isCache { options = append(options, storage.DisableDigestResumption) } // configure deletion if d, ok := config.Storage["delete"]; ok { e, ok := d["enabled"] if ok { if deleteEnabled, ok := e.(bool); ok && deleteEnabled { options = append(options, storage.EnableDelete) } } } // configure redirects var redirectDisabled bool if redirectConfig, ok := config.Storage["redirect"]; ok { v := redirectConfig["disable"] switch v := v.(type) { case bool: redirectDisabled = v default: panic(fmt.Sprintf("invalid type for redirect config: %#v", redirectConfig)) } } if redirectDisabled { ctxu.GetLogger(app).Infof("backend redirection disabled") } else { options = append(options, storage.EnableRedirect) } // configure validation if config.Validation.Enabled { if len(config.Validation.Manifests.URLs.Allow) == 0 && len(config.Validation.Manifests.URLs.Deny) == 0 { // If Allow and Deny are empty, allow nothing. options = append(options, storage.ManifestURLsAllowRegexp(regexp.MustCompile("^$"))) } else { if len(config.Validation.Manifests.URLs.Allow) > 0 { for i, s := range config.Validation.Manifests.URLs.Allow { // Validate via compilation. if _, err := regexp.Compile(s); err != nil { panic(fmt.Sprintf("validation.manifests.urls.allow: %s", err)) } // Wrap with non-capturing group. config.Validation.Manifests.URLs.Allow[i] = fmt.Sprintf("(?:%s)", s) } re := regexp.MustCompile(strings.Join(config.Validation.Manifests.URLs.Allow, "|")) options = append(options, storage.ManifestURLsAllowRegexp(re)) } if len(config.Validation.Manifests.URLs.Deny) > 0 { for i, s := range config.Validation.Manifests.URLs.Deny { // Validate via compilation. if _, err := regexp.Compile(s); err != nil { panic(fmt.Sprintf("validation.manifests.urls.deny: %s", err)) } // Wrap with non-capturing group. config.Validation.Manifests.URLs.Deny[i] = fmt.Sprintf("(?:%s)", s) } re := regexp.MustCompile(strings.Join(config.Validation.Manifests.URLs.Deny, "|")) options = append(options, storage.ManifestURLsDenyRegexp(re)) } } } // configure storage caches if cc, ok := config.Storage["cache"]; ok { v, ok := cc["blobdescriptor"] if !ok { // Backwards compatible: "layerinfo" == "blobdescriptor" v = cc["layerinfo"] } switch v { case "redis": if app.redis == nil { panic("redis configuration required to use for layerinfo cache") } cacheProvider := rediscache.NewRedisBlobDescriptorCacheProvider(app.redis) localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider)) app.registry, err = storage.NewRegistry(app, app.driver, localOptions...) if err != nil { panic("could not create registry: " + err.Error()) } ctxu.GetLogger(app).Infof("using redis blob descriptor cache") case "inmemory": cacheProvider := memorycache.NewInMemoryBlobDescriptorCacheProvider() localOptions := append(options, storage.BlobDescriptorCacheProvider(cacheProvider)) app.registry, err = storage.NewRegistry(app, app.driver, localOptions...) if err != nil { panic("could not create registry: " + err.Error()) } ctxu.GetLogger(app).Infof("using inmemory blob descriptor cache") default: if v != "" { ctxu.GetLogger(app).Warnf("unknown cache type %q, caching disabled", config.Storage["cache"]) } } } if app.registry == nil { // configure the registry if no cache section is available. app.registry, err = storage.NewRegistry(app.Context, app.driver, options...) if err != nil { panic("could not create registry: " + err.Error()) } } app.registry, err = applyRegistryMiddleware(app, app.registry, config.Middleware["registry"]) if err != nil { panic(err) } authType := config.Auth.Type() if authType != "" { accessController, err := auth.GetAccessController(config.Auth.Type(), config.Auth.Parameters()) if err != nil { panic(fmt.Sprintf("unable to configure authorization (%s): %v", authType, err)) } app.accessController = accessController ctxu.GetLogger(app).Debugf("configured %q access controller", authType) } // configure as a pull through cache if config.Proxy.RemoteURL != "" { app.registry, err = proxy.NewRegistryPullThroughCache(ctx, app.registry, app.driver, config.Proxy) if err != nil { panic(err.Error()) } app.isCache = true ctxu.GetLogger(app).Info("Registry configured as a proxy cache to ", config.Proxy.RemoteURL) } return app } // RegisterHealthChecks is an awful hack to defer health check registration // control to callers. This should only ever be called once per registry // process, typically in a main function. The correct way would be register // health checks outside of app, since multiple apps may exist in the same // process. Because the configuration and app are tightly coupled, // implementing this properly will require a refactor. This method may panic // if called twice in the same process. func (app *App) RegisterHealthChecks(healthRegistries ...*health.Registry) { if len(healthRegistries) > 1 { panic("RegisterHealthChecks called with more than one registry") } healthRegistry := health.DefaultRegistry if len(healthRegistries) == 1 { healthRegistry = healthRegistries[0] } if app.Config.Health.StorageDriver.Enabled { interval := app.Config.Health.StorageDriver.Interval if interval == 0 { interval = defaultCheckInterval } storageDriverCheck := func() error { _, err := app.driver.Stat(app, "/") // "/" should always exist return err // any error will be treated as failure } if app.Config.Health.StorageDriver.Threshold != 0 { healthRegistry.RegisterPeriodicThresholdFunc("storagedriver_"+app.Config.Storage.Type(), interval, app.Config.Health.StorageDriver.Threshold, storageDriverCheck) } else { healthRegistry.RegisterPeriodicFunc("storagedriver_"+app.Config.Storage.Type(), interval, storageDriverCheck) } } for _, fileChecker := range app.Config.Health.FileCheckers { interval := fileChecker.Interval if interval == 0 { interval = defaultCheckInterval } ctxu.GetLogger(app).Infof("configuring file health check path=%s, interval=%d", fileChecker.File, interval/time.Second) healthRegistry.Register(fileChecker.File, health.PeriodicChecker(checks.FileChecker(fileChecker.File), interval)) } for _, httpChecker := range app.Config.Health.HTTPCheckers { interval := httpChecker.Interval if interval == 0 { interval = defaultCheckInterval } statusCode := httpChecker.StatusCode if statusCode == 0 { statusCode = 200 } checker := checks.HTTPChecker(httpChecker.URI, statusCode, httpChecker.Timeout, httpChecker.Headers) if httpChecker.Threshold != 0 { ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d, threshold=%d", httpChecker.URI, interval/time.Second, httpChecker.Threshold) healthRegistry.Register(httpChecker.URI, health.PeriodicThresholdChecker(checker, interval, httpChecker.Threshold)) } else { ctxu.GetLogger(app).Infof("configuring HTTP health check uri=%s, interval=%d", httpChecker.URI, interval/time.Second) healthRegistry.Register(httpChecker.URI, health.PeriodicChecker(checker, interval)) } } for _, tcpChecker := range app.Config.Health.TCPCheckers { interval := tcpChecker.Interval if interval == 0 { interval = defaultCheckInterval } checker := checks.TCPChecker(tcpChecker.Addr, tcpChecker.Timeout) if tcpChecker.Threshold != 0 { ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d, threshold=%d", tcpChecker.Addr, interval/time.Second, tcpChecker.Threshold) healthRegistry.Register(tcpChecker.Addr, health.PeriodicThresholdChecker(checker, interval, tcpChecker.Threshold)) } else { ctxu.GetLogger(app).Infof("configuring TCP health check addr=%s, interval=%d", tcpChecker.Addr, interval/time.Second) healthRegistry.Register(tcpChecker.Addr, health.PeriodicChecker(checker, interval)) } } } // register a handler with the application, by route name. The handler will be // passed through the application filters and context will be constructed at // request time. func (app *App) register(routeName string, dispatch dispatchFunc) { // TODO(stevvooe): This odd dispatcher/route registration is by-product of // some limitations in the gorilla/mux router. We are using it to keep // routing consistent between the client and server, but we may want to // replace it with manual routing and structure-based dispatch for better // control over the request execution. app.router.GetRoute(routeName).Handler(app.dispatcher(dispatch)) } // configureEvents prepares the event sink for action. func (app *App) configureEvents(configuration *configuration.Configuration) { // Configure all of the endpoint sinks. var sinks []notifications.Sink for _, endpoint := range configuration.Notifications.Endpoints { if endpoint.Disabled { ctxu.GetLogger(app).Infof("endpoint %s disabled, skipping", endpoint.Name) continue } ctxu.GetLogger(app).Infof("configuring endpoint %v (%v), timeout=%s, headers=%v", endpoint.Name, endpoint.URL, endpoint.Timeout, endpoint.Headers) endpoint := notifications.NewEndpoint(endpoint.Name, endpoint.URL, notifications.EndpointConfig{ Timeout: endpoint.Timeout, Threshold: endpoint.Threshold, Backoff: endpoint.Backoff, Headers: endpoint.Headers, IgnoredMediaTypes: endpoint.IgnoredMediaTypes, }) sinks = append(sinks, endpoint) } // NOTE(stevvooe): Moving to a new queuing implementation is as easy as // replacing broadcaster with a rabbitmq implementation. It's recommended // that the registry instances also act as the workers to keep deployment // simple. app.events.sink = notifications.NewBroadcaster(sinks...) // Populate registry event source hostname, err := os.Hostname() if err != nil { hostname = configuration.HTTP.Addr } else { // try to pick the port off the config _, port, err := net.SplitHostPort(configuration.HTTP.Addr) if err == nil { hostname = net.JoinHostPort(hostname, port) } } app.events.source = notifications.SourceRecord{ Addr: hostname, InstanceID: ctxu.GetStringValue(app, "instance.id"), } } type redisStartAtKey struct{} func (app *App) configureRedis(configuration *configuration.Configuration) { if configuration.Redis.Addr == "" { ctxu.GetLogger(app).Infof("redis not configured") return } pool := &redis.Pool{ Dial: func() (redis.Conn, error) { // TODO(stevvooe): Yet another use case for contextual timing. ctx := context.WithValue(app, redisStartAtKey{}, time.Now()) done := func(err error) { logger := ctxu.GetLoggerWithField(ctx, "redis.connect.duration", ctxu.Since(ctx, redisStartAtKey{})) if err != nil { logger.Errorf("redis: error connecting: %v", err) } else { logger.Infof("redis: connect %v", configuration.Redis.Addr) } } conn, err := redis.DialTimeout("tcp", configuration.Redis.Addr, configuration.Redis.DialTimeout, configuration.Redis.ReadTimeout, configuration.Redis.WriteTimeout) if err != nil { ctxu.GetLogger(app).Errorf("error connecting to redis instance %s: %v", configuration.Redis.Addr, err) done(err) return nil, err } // authorize the connection if configuration.Redis.Password != "" { if _, err = conn.Do("AUTH", configuration.Redis.Password); err != nil { defer conn.Close() done(err) return nil, err } } // select the database to use if configuration.Redis.DB != 0 { if _, err = conn.Do("SELECT", configuration.Redis.DB); err != nil { defer conn.Close() done(err) return nil, err } } done(nil) return conn, nil }, MaxIdle: configuration.Redis.Pool.MaxIdle, MaxActive: configuration.Redis.Pool.MaxActive, IdleTimeout: configuration.Redis.Pool.IdleTimeout, TestOnBorrow: func(c redis.Conn, t time.Time) error { // TODO(stevvooe): We can probably do something more interesting // here with the health package. _, err := c.Do("PING") return err }, Wait: false, // if a connection is not avialable, proceed without cache. } app.redis = pool // setup expvar registry := expvar.Get("registry") if registry == nil { registry = expvar.NewMap("registry") } registry.(*expvar.Map).Set("redis", expvar.Func(func() interface{} { return map[string]interface{}{ "Config": configuration.Redis, "Active": app.redis.ActiveCount(), } })) } // configureLogHook prepares logging hook parameters. func (app *App) configureLogHook(configuration *configuration.Configuration) { entry, ok := ctxu.GetLogger(app).(*log.Entry) if !ok { // somehow, we are not using logrus return } logger := entry.Logger for _, configHook := range configuration.Log.Hooks { if !configHook.Disabled { switch configHook.Type { case "mail": hook := &logHook{} hook.LevelsParam = configHook.Levels hook.Mail = &mailer{ Addr: configHook.MailOptions.SMTP.Addr, Username: configHook.MailOptions.SMTP.Username, Password: configHook.MailOptions.SMTP.Password, Insecure: configHook.MailOptions.SMTP.Insecure, From: configHook.MailOptions.From, To: configHook.MailOptions.To, } logger.Hooks.Add(hook) default: } } } } // configureSecret creates a random secret if a secret wasn't included in the // configuration. func (app *App) configureSecret(configuration *configuration.Configuration) { if configuration.HTTP.Secret == "" { var secretBytes [randomSecretSize]byte if _, err := cryptorand.Read(secretBytes[:]); err != nil { panic(fmt.Sprintf("could not generate random bytes for HTTP secret: %v", err)) } configuration.HTTP.Secret = string(secretBytes[:]) ctxu.GetLogger(app).Warn("No HTTP secret provided - generated random secret. This may cause problems with uploads if multiple registries are behind a load-balancer. To provide a shared secret, fill in http.secret in the configuration file or set the REGISTRY_HTTP_SECRET environment variable.") } } func (app *App) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() // ensure that request body is always closed. // Instantiate an http context here so we can track the error codes // returned by the request router. ctx := defaultContextManager.context(app, w, r) defer func() { status, ok := ctx.Value("http.response.status").(int) if ok && status >= 200 && status <= 399 { ctxu.GetResponseLogger(ctx).Infof("response completed") } }() defer defaultContextManager.release(ctx) // NOTE(stevvooe): Total hack to get instrumented responsewriter from context. var err error w, err = ctxu.GetResponseWriter(ctx) if err != nil { ctxu.GetLogger(ctx).Warnf("response writer not found in context") } // Set a header with the Docker Distribution API Version for all responses. w.Header().Add("Docker-Distribution-API-Version", "registry/2.0") app.router.ServeHTTP(w, r) } // dispatchFunc takes a context and request and returns a constructed handler // for the route. The dispatcher will use this to dynamically create request // specific handlers for each endpoint without creating a new router for each // request. type dispatchFunc func(ctx *Context, r *http.Request) http.Handler // TODO(stevvooe): dispatchers should probably have some validation error // chain with proper error reporting. // dispatcher returns a handler that constructs a request specific context and // handler, using the dispatch factory function. func (app *App) dispatcher(dispatch dispatchFunc) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for headerName, headerValues := range app.Config.HTTP.Headers { for _, value := range headerValues { w.Header().Add(headerName, value) } } context := app.context(w, r) if err := app.authorized(w, r, context); err != nil { ctxu.GetLogger(context).Warnf("error authorizing context: %v", err) return } // Add username to request logging context.Context = ctxu.WithLogger(context.Context, ctxu.GetLogger(context.Context, auth.UserNameKey)) if app.nameRequired(r) { nameRef, err := reference.ParseNamed(getName(context)) if err != nil { ctxu.GetLogger(context).Errorf("error parsing reference from context: %v", err) context.Errors = append(context.Errors, distribution.ErrRepositoryNameInvalid{ Name: getName(context), Reason: err, }) if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return } repository, err := app.registry.Repository(context, nameRef) if err != nil { ctxu.GetLogger(context).Errorf("error resolving repository: %v", err) switch err := err.(type) { case distribution.ErrRepositoryUnknown: context.Errors = append(context.Errors, v2.ErrorCodeNameUnknown.WithDetail(err)) case distribution.ErrRepositoryNameInvalid: context.Errors = append(context.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) case errcode.Error: context.Errors = append(context.Errors, err) } if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return } // assign and decorate the authorized repository with an event bridge. context.Repository = notifications.Listen( repository, app.eventBridge(context, r)) context.Repository, err = applyRepoMiddleware(app, context.Repository, app.Config.Middleware["repository"]) if err != nil { ctxu.GetLogger(context).Errorf("error initializing repository middleware: %v", err) context.Errors = append(context.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return } } dispatch(context, r).ServeHTTP(w, r) // Automated error response handling here. Handlers may return their // own errors if they need different behavior (such as range errors // for layer upload). if context.Errors.Len() > 0 { if err := errcode.ServeJSON(w, context.Errors); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } app.logError(context, context.Errors) } }) } type errCodeKey struct{} func (errCodeKey) String() string { return "err.code" } type errMessageKey struct{} func (errMessageKey) String() string { return "err.message" } type errDetailKey struct{} func (errDetailKey) String() string { return "err.detail" } func (app *App) logError(context context.Context, errors errcode.Errors) { for _, e1 := range errors { var c ctxu.Context switch e1.(type) { case errcode.Error: e, _ := e1.(errcode.Error) c = ctxu.WithValue(context, errCodeKey{}, e.Code) c = ctxu.WithValue(c, errMessageKey{}, e.Code.Message()) c = ctxu.WithValue(c, errDetailKey{}, e.Detail) case errcode.ErrorCode: e, _ := e1.(errcode.ErrorCode) c = ctxu.WithValue(context, errCodeKey{}, e) c = ctxu.WithValue(c, errMessageKey{}, e.Message()) default: // just normal go 'error' c = ctxu.WithValue(context, errCodeKey{}, errcode.ErrorCodeUnknown) c = ctxu.WithValue(c, errMessageKey{}, e1.Error()) } c = ctxu.WithLogger(c, ctxu.GetLogger(c, errCodeKey{}, errMessageKey{}, errDetailKey{})) ctxu.GetResponseLogger(c).Errorf("response completed with error") } } // context constructs the context object for the application. This only be // called once per request. func (app *App) context(w http.ResponseWriter, r *http.Request) *Context { ctx := defaultContextManager.context(app, w, r) ctx = ctxu.WithVars(ctx, r) ctx = ctxu.WithLogger(ctx, ctxu.GetLogger(ctx, "vars.name", "vars.reference", "vars.digest", "vars.uuid")) context := &Context{ App: app, Context: ctx, } if app.httpHost.Scheme != "" && app.httpHost.Host != "" { // A "host" item in the configuration takes precedence over // X-Forwarded-Proto and X-Forwarded-Host headers, and the // hostname in the request. context.urlBuilder = v2.NewURLBuilder(&app.httpHost, false) } else { context.urlBuilder = v2.NewURLBuilderFromRequest(r, app.Config.HTTP.RelativeURLs) } return context } // authorized checks if the request can proceed with access to the requested // repository. If it succeeds, the context may access the requested // repository. An error will be returned if access is not available. func (app *App) authorized(w http.ResponseWriter, r *http.Request, context *Context) error { ctxu.GetLogger(context).Debug("authorizing request") repo := getName(context) if app.accessController == nil { return nil // access controller is not enabled. } var accessRecords []auth.Access if repo != "" { accessRecords = appendAccessRecords(accessRecords, r.Method, repo) if fromRepo := r.FormValue("from"); fromRepo != "" { // mounting a blob from one repository to another requires pull (GET) // access to the source repository. accessRecords = appendAccessRecords(accessRecords, "GET", fromRepo) } } else { // Only allow the name not to be set on the base route. if app.nameRequired(r) { // For this to be properly secured, repo must always be set for a // resource that may make a modification. The only condition under // which name is not set and we still allow access is when the // base route is accessed. This section prevents us from making // that mistake elsewhere in the code, allowing any operation to // proceed. if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } return fmt.Errorf("forbidden: no repository name") } accessRecords = appendCatalogAccessRecord(accessRecords, r) } ctx, err := app.accessController.Authorized(context.Context, accessRecords...) if err != nil { switch err := err.(type) { case auth.Challenge: // Add the appropriate WWW-Auth header err.SetHeaders(w) if err := errcode.ServeJSON(w, errcode.ErrorCodeUnauthorized.WithDetail(accessRecords)); err != nil { ctxu.GetLogger(context).Errorf("error serving error json: %v (from %v)", err, context.Errors) } default: // This condition is a potential security problem either in // the configuration or whatever is backing the access // controller. Just return a bad request with no information // to avoid exposure. The request should not proceed. ctxu.GetLogger(context).Errorf("error checking authorization: %v", err) w.WriteHeader(http.StatusBadRequest) } return err } // TODO(stevvooe): This pattern needs to be cleaned up a bit. One context // should be replaced by another, rather than replacing the context on a // mutable object. context.Context = ctx return nil } // eventBridge returns a bridge for the current request, configured with the // correct actor and source. func (app *App) eventBridge(ctx *Context, r *http.Request) notifications.Listener { actor := notifications.ActorRecord{ Name: getUserName(ctx, r), } request := notifications.NewRequestRecord(ctxu.GetRequestID(ctx), r) return notifications.NewBridge(ctx.urlBuilder, app.events.source, actor, request, app.events.sink) } // nameRequired returns true if the route requires a name. func (app *App) nameRequired(r *http.Request) bool { route := mux.CurrentRoute(r) routeName := route.GetName() return route == nil || (routeName != v2.RouteNameBase && routeName != v2.RouteNameCatalog) } // apiBase implements a simple yes-man for doing overall checks against the // api. This can support auth roundtrips to support docker login. func apiBase(w http.ResponseWriter, r *http.Request) { const emptyJSON = "{}" // Provide a simple /v2/ 200 OK response with empty json response. w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Length", fmt.Sprint(len(emptyJSON))) fmt.Fprint(w, emptyJSON) } // appendAccessRecords checks the method and adds the appropriate Access records to the records list. func appendAccessRecords(records []auth.Access, method string, repo string) []auth.Access { resource := auth.Resource{ Type: "repository", Name: repo, } switch method { case "GET", "HEAD": records = append(records, auth.Access{ Resource: resource, Action: "pull", }) case "POST", "PUT", "PATCH": records = append(records, auth.Access{ Resource: resource, Action: "pull", }, auth.Access{ Resource: resource, Action: "push", }) case "DELETE": // DELETE access requires full admin rights, which is represented // as "*". This may not be ideal. records = append(records, auth.Access{ Resource: resource, Action: "*", }) } return records } // Add the access record for the catalog if it's our current route func appendCatalogAccessRecord(accessRecords []auth.Access, r *http.Request) []auth.Access { route := mux.CurrentRoute(r) routeName := route.GetName() if routeName == v2.RouteNameCatalog { resource := auth.Resource{ Type: "registry", Name: "catalog", } accessRecords = append(accessRecords, auth.Access{ Resource: resource, Action: "*", }) } return accessRecords } // applyRegistryMiddleware wraps a registry instance with the configured middlewares func applyRegistryMiddleware(ctx context.Context, registry distribution.Namespace, middlewares []configuration.Middleware) (distribution.Namespace, error) { for _, mw := range middlewares { rmw, err := registrymiddleware.Get(ctx, mw.Name, mw.Options, registry) if err != nil { return nil, fmt.Errorf("unable to configure registry middleware (%s): %s", mw.Name, err) } registry = rmw } return registry, nil } // applyRepoMiddleware wraps a repository with the configured middlewares func applyRepoMiddleware(ctx context.Context, repository distribution.Repository, middlewares []configuration.Middleware) (distribution.Repository, error) { for _, mw := range middlewares { rmw, err := repositorymiddleware.Get(ctx, mw.Name, mw.Options, repository) if err != nil { return nil, err } repository = rmw } return repository, nil } // applyStorageMiddleware wraps a storage driver with the configured middlewares func applyStorageMiddleware(driver storagedriver.StorageDriver, middlewares []configuration.Middleware) (storagedriver.StorageDriver, error) { for _, mw := range middlewares { smw, err := storagemiddleware.Get(mw.Name, mw.Options, driver) if err != nil { return nil, fmt.Errorf("unable to configure storage middleware (%s): %v", mw.Name, err) } driver = smw } return driver, nil } // uploadPurgeDefaultConfig provides a default configuration for upload // purging to be used in the absence of configuration in the // confifuration file func uploadPurgeDefaultConfig() map[interface{}]interface{} { config := map[interface{}]interface{}{} config["enabled"] = true config["age"] = "168h" config["interval"] = "24h" config["dryrun"] = false return config } func badPurgeUploadConfig(reason string) { panic(fmt.Sprintf("Unable to parse upload purge configuration: %s", reason)) } // startUploadPurger schedules a goroutine which will periodically // check upload directories for old files and delete them func startUploadPurger(ctx context.Context, storageDriver storagedriver.StorageDriver, log ctxu.Logger, config map[interface{}]interface{}) { if config["enabled"] == false { return } var purgeAgeDuration time.Duration var err error purgeAge, ok := config["age"] if ok { ageStr, ok := purgeAge.(string) if !ok { badPurgeUploadConfig("age is not a string") } purgeAgeDuration, err = time.ParseDuration(ageStr) if err != nil { badPurgeUploadConfig(fmt.Sprintf("Cannot parse duration: %s", err.Error())) } } else { badPurgeUploadConfig("age missing") } var intervalDuration time.Duration interval, ok := config["interval"] if ok { intervalStr, ok := interval.(string) if !ok { badPurgeUploadConfig("interval is not a string") } intervalDuration, err = time.ParseDuration(intervalStr) if err != nil { badPurgeUploadConfig(fmt.Sprintf("Cannot parse interval: %s", err.Error())) } } else { badPurgeUploadConfig("interval missing") } var dryRunBool bool dryRun, ok := config["dryrun"] if ok { dryRunBool, ok = dryRun.(bool) if !ok { badPurgeUploadConfig("cannot parse dryrun") } } else { badPurgeUploadConfig("dryrun missing") } go func() { rand.Seed(time.Now().Unix()) jitter := time.Duration(rand.Int()%60) * time.Minute log.Infof("Starting upload purge in %s", jitter) time.Sleep(jitter) for { storage.PurgeUploads(ctx, storageDriver, time.Now().Add(-purgeAgeDuration), !dryRunBool) log.Infof("Starting upload purge in %s", intervalDuration) time.Sleep(intervalDuration) } }() } docker-registry-2.6.2~ds1/registry/handlers/app_test.go000066400000000000000000000175531313450123100232660ustar00rootroot00000000000000package handlers import ( "encoding/json" "net/http" "net/http/httptest" "net/url" "reflect" "testing" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/auth" _ "github.com/docker/distribution/registry/auth/silly" "github.com/docker/distribution/registry/storage" memorycache "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/testdriver" ) // TestAppDispatcher builds an application with a test dispatcher and ensures // that requests are properly dispatched and the handlers are constructed. // This only tests the dispatch mechanism. The underlying dispatchers must be // tested individually. func TestAppDispatcher(t *testing.T) { driver := testdriver.New() ctx := context.Background() registry, err := storage.NewRegistry(ctx, driver, storage.BlobDescriptorCacheProvider(memorycache.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableDelete, storage.EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } app := &App{ Config: &configuration.Configuration{}, Context: ctx, router: v2.Router(), driver: driver, registry: registry, } server := httptest.NewServer(app) defer server.Close() router := v2.Router() serverURL, err := url.Parse(server.URL) if err != nil { t.Fatalf("error parsing server url: %v", err) } varCheckingDispatcher := func(expectedVars map[string]string) dispatchFunc { return func(ctx *Context, r *http.Request) http.Handler { // Always checks the same name context if ctx.Repository.Named().Name() != getName(ctx) { t.Fatalf("unexpected name: %q != %q", ctx.Repository.Named().Name(), "foo/bar") } // Check that we have all that is expected for expectedK, expectedV := range expectedVars { if ctx.Value(expectedK) != expectedV { t.Fatalf("unexpected %s in context vars: %q != %q", expectedK, ctx.Value(expectedK), expectedV) } } // Check that we only have variables that are expected for k, v := range ctx.Value("vars").(map[string]string) { _, ok := expectedVars[k] if !ok { // name is checked on context // We have an unexpected key, fail t.Fatalf("unexpected key %q in vars with value %q", k, v) } } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) } } // unflatten a list of variables, suitable for gorilla/mux, to a map[string]string unflatten := func(vars []string) map[string]string { m := make(map[string]string) for i := 0; i < len(vars)-1; i = i + 2 { m[vars[i]] = vars[i+1] } return m } for _, testcase := range []struct { endpoint string vars []string }{ { endpoint: v2.RouteNameManifest, vars: []string{ "name", "foo/bar", "reference", "sometag", }, }, { endpoint: v2.RouteNameTags, vars: []string{ "name", "foo/bar", }, }, { endpoint: v2.RouteNameBlobUpload, vars: []string{ "name", "foo/bar", }, }, { endpoint: v2.RouteNameBlobUploadChunk, vars: []string{ "name", "foo/bar", "uuid", "theuuid", }, }, } { app.register(testcase.endpoint, varCheckingDispatcher(unflatten(testcase.vars))) route := router.GetRoute(testcase.endpoint).Host(serverURL.Host) u, err := route.URL(testcase.vars...) if err != nil { t.Fatal(err) } resp, err := http.Get(u.String()) if err != nil { t.Fatal(err) } if resp.StatusCode != http.StatusOK { t.Fatalf("unexpected status code: %v != %v", resp.StatusCode, http.StatusOK) } } } // TestNewApp covers the creation of an application via NewApp with a // configuration. func TestNewApp(t *testing.T) { ctx := context.Background() config := configuration.Configuration{ Storage: configuration.Storage{ "testdriver": nil, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, Auth: configuration.Auth{ // For now, we simply test that new auth results in a viable // application. "silly": { "realm": "realm-test", "service": "service-test", }, }, } // Mostly, with this test, given a sane configuration, we are simply // ensuring that NewApp doesn't panic. We might want to tweak this // behavior. app := NewApp(ctx, &config) server := httptest.NewServer(app) defer server.Close() builder, err := v2.NewURLBuilderFromString(server.URL, false) if err != nil { t.Fatalf("error creating urlbuilder: %v", err) } baseURL, err := builder.BuildBaseURL() if err != nil { t.Fatalf("error creating baseURL: %v", err) } // TODO(stevvooe): The rest of this test might belong in the API tests. // Just hit the app and make sure we get a 401 Unauthorized error. req, err := http.Get(baseURL) if err != nil { t.Fatalf("unexpected error during GET: %v", err) } defer req.Body.Close() if req.StatusCode != http.StatusUnauthorized { t.Fatalf("unexpected status code during request: %v", err) } if req.Header.Get("Content-Type") != "application/json; charset=utf-8" { t.Fatalf("unexpected content-type: %v != %v", req.Header.Get("Content-Type"), "application/json; charset=utf-8") } expectedAuthHeader := "Bearer realm=\"realm-test\",service=\"service-test\"" if e, a := expectedAuthHeader, req.Header.Get("WWW-Authenticate"); e != a { t.Fatalf("unexpected WWW-Authenticate header: %q != %q", e, a) } var errs errcode.Errors dec := json.NewDecoder(req.Body) if err := dec.Decode(&errs); err != nil { t.Fatalf("error decoding error response: %v", err) } err2, ok := errs[0].(errcode.ErrorCoder) if !ok { t.Fatalf("not an ErrorCoder: %#v", errs[0]) } if err2.ErrorCode() != errcode.ErrorCodeUnauthorized { t.Fatalf("unexpected error code: %v != %v", err2.ErrorCode(), errcode.ErrorCodeUnauthorized) } } // Test the access record accumulator func TestAppendAccessRecords(t *testing.T) { repo := "testRepo" expectedResource := auth.Resource{ Type: "repository", Name: repo, } expectedPullRecord := auth.Access{ Resource: expectedResource, Action: "pull", } expectedPushRecord := auth.Access{ Resource: expectedResource, Action: "push", } expectedAllRecord := auth.Access{ Resource: expectedResource, Action: "*", } records := []auth.Access{} result := appendAccessRecords(records, "GET", repo) expectedResult := []auth.Access{expectedPullRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } records = []auth.Access{} result = appendAccessRecords(records, "HEAD", repo) expectedResult = []auth.Access{expectedPullRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } records = []auth.Access{} result = appendAccessRecords(records, "POST", repo) expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } records = []auth.Access{} result = appendAccessRecords(records, "PUT", repo) expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } records = []auth.Access{} result = appendAccessRecords(records, "PATCH", repo) expectedResult = []auth.Access{expectedPullRecord, expectedPushRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } records = []auth.Access{} result = appendAccessRecords(records, "DELETE", repo) expectedResult = []auth.Access{expectedAllRecord} if ok := reflect.DeepEqual(result, expectedResult); !ok { t.Fatalf("Actual access record differs from expected") } } docker-registry-2.6.2~ds1/registry/handlers/basicauth.go000066400000000000000000000002321313450123100233740ustar00rootroot00000000000000// +build go1.4 package handlers import ( "net/http" ) func basicAuth(r *http.Request) (username, password string, ok bool) { return r.BasicAuth() } docker-registry-2.6.2~ds1/registry/handlers/basicauth_prego14.go000066400000000000000000000020121313450123100247330ustar00rootroot00000000000000// +build !go1.4 package handlers import ( "encoding/base64" "net/http" "strings" ) // NOTE(stevvooe): This is basic auth support from go1.4 present to ensure we // can compile on go1.3 and earlier. // BasicAuth returns the username and password provided in the request's // Authorization header, if the request uses HTTP Basic Authentication. // See RFC 2617, Section 2. func basicAuth(r *http.Request) (username, password string, ok bool) { auth := r.Header.Get("Authorization") if auth == "" { return } return parseBasicAuth(auth) } // parseBasicAuth parses an HTTP Basic Authentication string. // "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). func parseBasicAuth(auth string) (username, password string, ok bool) { if !strings.HasPrefix(auth, "Basic ") { return } c, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(auth, "Basic ")) if err != nil { return } cs := string(c) s := strings.IndexByte(cs, ':') if s < 0 { return } return cs[:s], cs[s+1:], true } docker-registry-2.6.2~ds1/registry/handlers/blob.go000066400000000000000000000052171313450123100223570ustar00rootroot00000000000000package handlers import ( "net/http" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/gorilla/handlers" ) // blobDispatcher uses the request context to build a blobHandler. func blobDispatcher(ctx *Context, r *http.Request) http.Handler { dgst, err := getDigest(ctx) if err != nil { if err == errDigestNotAvailable { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx.Errors = append(ctx.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) }) } blobHandler := &blobHandler{ Context: ctx, Digest: dgst, } mhandler := handlers.MethodHandler{ "GET": http.HandlerFunc(blobHandler.GetBlob), "HEAD": http.HandlerFunc(blobHandler.GetBlob), } if !ctx.readOnly { mhandler["DELETE"] = http.HandlerFunc(blobHandler.DeleteBlob) } return mhandler } // blobHandler serves http blob requests. type blobHandler struct { *Context Digest digest.Digest } // GetBlob fetches the binary data from backend storage returns it in the // response. func (bh *blobHandler) GetBlob(w http.ResponseWriter, r *http.Request) { context.GetLogger(bh).Debug("GetBlob") blobs := bh.Repository.Blobs(bh) desc, err := blobs.Stat(bh, bh.Digest) if err != nil { if err == distribution.ErrBlobUnknown { bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown.WithDetail(bh.Digest)) } else { bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } return } if err := blobs.ServeBlob(bh, w, r, desc.Digest); err != nil { context.GetLogger(bh).Debugf("unexpected error getting blob HTTP handler: %v", err) bh.Errors = append(bh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } // DeleteBlob deletes a layer blob func (bh *blobHandler) DeleteBlob(w http.ResponseWriter, r *http.Request) { context.GetLogger(bh).Debug("DeleteBlob") blobs := bh.Repository.Blobs(bh) err := blobs.Delete(bh, bh.Digest) if err != nil { switch err { case distribution.ErrUnsupported: bh.Errors = append(bh.Errors, errcode.ErrorCodeUnsupported) return case distribution.ErrBlobUnknown: bh.Errors = append(bh.Errors, v2.ErrorCodeBlobUnknown) return default: bh.Errors = append(bh.Errors, err) context.GetLogger(bh).Errorf("Unknown error deleting blob: %s", err.Error()) return } } w.Header().Set("Content-Length", "0") w.WriteHeader(http.StatusAccepted) } docker-registry-2.6.2~ds1/registry/handlers/blobupload.go000066400000000000000000000277251313450123100235740ustar00rootroot00000000000000package handlers import ( "fmt" "net/http" "net/url" "github.com/docker/distribution" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/storage" "github.com/gorilla/handlers" ) // blobUploadDispatcher constructs and returns the blob upload handler for the // given request context. func blobUploadDispatcher(ctx *Context, r *http.Request) http.Handler { buh := &blobUploadHandler{ Context: ctx, UUID: getUploadUUID(ctx), } handler := handlers.MethodHandler{ "GET": http.HandlerFunc(buh.GetUploadStatus), "HEAD": http.HandlerFunc(buh.GetUploadStatus), } if !ctx.readOnly { handler["POST"] = http.HandlerFunc(buh.StartBlobUpload) handler["PATCH"] = http.HandlerFunc(buh.PatchBlobData) handler["PUT"] = http.HandlerFunc(buh.PutBlobUploadComplete) handler["DELETE"] = http.HandlerFunc(buh.CancelBlobUpload) } if buh.UUID != "" { state, err := hmacKey(ctx.Config.HTTP.Secret).unpackUploadState(r.FormValue("_state")) if err != nil { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(ctx).Infof("error resolving upload: %v", err) buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) }) } buh.State = state if state.Name != ctx.Repository.Named().Name() { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(ctx).Infof("mismatched repository name in upload state: %q != %q", state.Name, buh.Repository.Named().Name()) buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) }) } if state.UUID != buh.UUID { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(ctx).Infof("mismatched uuid in upload state: %q != %q", state.UUID, buh.UUID) buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) }) } blobs := ctx.Repository.Blobs(buh) upload, err := blobs.Resume(buh, buh.UUID) if err != nil { ctxu.GetLogger(ctx).Errorf("error resolving upload: %v", err) if err == distribution.ErrBlobUploadUnknown { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown.WithDetail(err)) }) } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) }) } buh.Upload = upload if size := upload.Size(); size != buh.State.Offset { defer upload.Close() ctxu.GetLogger(ctx).Errorf("upload resumed at wrong offest: %d != %d", size, buh.State.Offset) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) upload.Cancel(buh) }) } return closeResources(handler, buh.Upload) } return handler } // blobUploadHandler handles the http blob upload process. type blobUploadHandler struct { *Context // UUID identifies the upload instance for the current request. Using UUID // to key blob writers since this implementation uses UUIDs. UUID string Upload distribution.BlobWriter State blobUploadState } // StartBlobUpload begins the blob upload process and allocates a server-side // blob writer session, optionally mounting the blob from a separate repository. func (buh *blobUploadHandler) StartBlobUpload(w http.ResponseWriter, r *http.Request) { var options []distribution.BlobCreateOption fromRepo := r.FormValue("from") mountDigest := r.FormValue("mount") if mountDigest != "" && fromRepo != "" { opt, err := buh.createBlobMountOption(fromRepo, mountDigest) if opt != nil && err == nil { options = append(options, opt) } } blobs := buh.Repository.Blobs(buh) upload, err := blobs.Create(buh, options...) if err != nil { if ebm, ok := err.(distribution.ErrBlobMounted); ok { if err := buh.writeBlobCreatedHeaders(w, ebm.Descriptor); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } } else if err == distribution.ErrUnsupported { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) } else { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } return } buh.Upload = upload if err := buh.blobUploadResponse(w, r, true); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } w.Header().Set("Docker-Upload-UUID", buh.Upload.ID()) w.WriteHeader(http.StatusAccepted) } // GetUploadStatus returns the status of a given upload, identified by id. func (buh *blobUploadHandler) GetUploadStatus(w http.ResponseWriter, r *http.Request) { if buh.Upload == nil { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) return } // TODO(dmcgowan): Set last argument to false in blobUploadResponse when // resumable upload is supported. This will enable returning a non-zero // range for clients to begin uploading at an offset. if err := buh.blobUploadResponse(w, r, true); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } w.Header().Set("Docker-Upload-UUID", buh.UUID) w.WriteHeader(http.StatusNoContent) } // PatchBlobData writes data to an upload. func (buh *blobUploadHandler) PatchBlobData(w http.ResponseWriter, r *http.Request) { if buh.Upload == nil { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) return } ct := r.Header.Get("Content-Type") if ct != "" && ct != "application/octet-stream" { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(fmt.Errorf("Bad Content-Type"))) // TODO(dmcgowan): encode error return } // TODO(dmcgowan): support Content-Range header to seek and write range if err := copyFullPayload(w, r, buh.Upload, -1, buh, "blob PATCH"); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) return } if err := buh.blobUploadResponse(w, r, false); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } w.WriteHeader(http.StatusAccepted) } // PutBlobUploadComplete takes the final request of a blob upload. The // request may include all the blob data or no blob data. Any data // provided is received and verified. If successful, the blob is linked // into the blob store and 201 Created is returned with the canonical // url of the blob. func (buh *blobUploadHandler) PutBlobUploadComplete(w http.ResponseWriter, r *http.Request) { if buh.Upload == nil { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) return } dgstStr := r.FormValue("digest") // TODO(stevvooe): Support multiple digest parameters! if dgstStr == "" { // no digest? return error, but allow retry. buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest missing")) return } dgst, err := digest.ParseDigest(dgstStr) if err != nil { // no digest? return error, but allow retry. buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail("digest parsing failed")) return } if err := copyFullPayload(w, r, buh.Upload, -1, buh, "blob PUT"); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err.Error())) return } desc, err := buh.Upload.Commit(buh, distribution.Descriptor{ Digest: dgst, // TODO(stevvooe): This isn't wildly important yet, but we should // really set the mediatype. For now, we can let the backend take care // of this. }) if err != nil { switch err := err.(type) { case distribution.ErrBlobInvalidDigest: buh.Errors = append(buh.Errors, v2.ErrorCodeDigestInvalid.WithDetail(err)) case errcode.Error: buh.Errors = append(buh.Errors, err) default: switch err { case distribution.ErrAccessDenied: buh.Errors = append(buh.Errors, errcode.ErrorCodeDenied) case distribution.ErrUnsupported: buh.Errors = append(buh.Errors, errcode.ErrorCodeUnsupported) case distribution.ErrBlobInvalidLength, distribution.ErrBlobDigestUnsupported: buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadInvalid.WithDetail(err)) default: ctxu.GetLogger(buh).Errorf("unknown error completing upload: %v", err) buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } } // Clean up the backend blob data if there was an error. if err := buh.Upload.Cancel(buh); err != nil { // If the cleanup fails, all we can do is observe and report. ctxu.GetLogger(buh).Errorf("error canceling upload after error: %v", err) } return } if err := buh.writeBlobCreatedHeaders(w, desc); err != nil { buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } // CancelBlobUpload cancels an in-progress upload of a blob. func (buh *blobUploadHandler) CancelBlobUpload(w http.ResponseWriter, r *http.Request) { if buh.Upload == nil { buh.Errors = append(buh.Errors, v2.ErrorCodeBlobUploadUnknown) return } w.Header().Set("Docker-Upload-UUID", buh.UUID) if err := buh.Upload.Cancel(buh); err != nil { ctxu.GetLogger(buh).Errorf("error encountered canceling upload: %v", err) buh.Errors = append(buh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } w.WriteHeader(http.StatusNoContent) } // blobUploadResponse provides a standard request for uploading blobs and // chunk responses. This sets the correct headers but the response status is // left to the caller. The fresh argument is used to ensure that new blob // uploads always start at a 0 offset. This allows disabling resumable push by // always returning a 0 offset on check status. func (buh *blobUploadHandler) blobUploadResponse(w http.ResponseWriter, r *http.Request, fresh bool) error { // TODO(stevvooe): Need a better way to manage the upload state automatically. buh.State.Name = buh.Repository.Named().Name() buh.State.UUID = buh.Upload.ID() buh.Upload.Close() buh.State.Offset = buh.Upload.Size() buh.State.StartedAt = buh.Upload.StartedAt() token, err := hmacKey(buh.Config.HTTP.Secret).packUploadState(buh.State) if err != nil { ctxu.GetLogger(buh).Infof("error building upload state token: %s", err) return err } uploadURL, err := buh.urlBuilder.BuildBlobUploadChunkURL( buh.Repository.Named(), buh.Upload.ID(), url.Values{ "_state": []string{token}, }) if err != nil { ctxu.GetLogger(buh).Infof("error building upload url: %s", err) return err } endRange := buh.Upload.Size() if endRange > 0 { endRange = endRange - 1 } w.Header().Set("Docker-Upload-UUID", buh.UUID) w.Header().Set("Location", uploadURL) w.Header().Set("Content-Length", "0") w.Header().Set("Range", fmt.Sprintf("0-%d", endRange)) return nil } // mountBlob attempts to mount a blob from another repository by its digest. If // successful, the blob is linked into the blob store and 201 Created is // returned with the canonical url of the blob. func (buh *blobUploadHandler) createBlobMountOption(fromRepo, mountDigest string) (distribution.BlobCreateOption, error) { dgst, err := digest.ParseDigest(mountDigest) if err != nil { return nil, err } ref, err := reference.ParseNamed(fromRepo) if err != nil { return nil, err } canonical, err := reference.WithDigest(ref, dgst) if err != nil { return nil, err } return storage.WithMountFrom(canonical), nil } // writeBlobCreatedHeaders writes the standard headers describing a newly // created blob. A 201 Created is written as well as the canonical URL and // blob digest. func (buh *blobUploadHandler) writeBlobCreatedHeaders(w http.ResponseWriter, desc distribution.Descriptor) error { ref, err := reference.WithDigest(buh.Repository.Named(), desc.Digest) if err != nil { return err } blobURL, err := buh.urlBuilder.BuildBlobURL(ref) if err != nil { return err } w.Header().Set("Location", blobURL) w.Header().Set("Content-Length", "0") w.Header().Set("Docker-Content-Digest", desc.Digest.String()) w.WriteHeader(http.StatusCreated) return nil } docker-registry-2.6.2~ds1/registry/handlers/catalog.go000066400000000000000000000044041313450123100230500ustar00rootroot00000000000000package handlers import ( "encoding/json" "fmt" "io" "net/http" "net/url" "strconv" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/storage/driver" "github.com/gorilla/handlers" ) const maximumReturnedEntries = 100 func catalogDispatcher(ctx *Context, r *http.Request) http.Handler { catalogHandler := &catalogHandler{ Context: ctx, } return handlers.MethodHandler{ "GET": http.HandlerFunc(catalogHandler.GetCatalog), } } type catalogHandler struct { *Context } type catalogAPIResponse struct { Repositories []string `json:"repositories"` } func (ch *catalogHandler) GetCatalog(w http.ResponseWriter, r *http.Request) { var moreEntries = true q := r.URL.Query() lastEntry := q.Get("last") maxEntries, err := strconv.Atoi(q.Get("n")) if err != nil || maxEntries < 0 { maxEntries = maximumReturnedEntries } repos := make([]string, maxEntries) filled, err := ch.App.registry.Repositories(ch.Context, repos, lastEntry) _, pathNotFound := err.(driver.PathNotFoundError) if err == io.EOF || pathNotFound { moreEntries = false } else if err != nil { ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } w.Header().Set("Content-Type", "application/json; charset=utf-8") // Add a link header if there are more entries to retrieve if moreEntries { lastEntry = repos[len(repos)-1] urlStr, err := createLinkEntry(r.URL.String(), maxEntries, lastEntry) if err != nil { ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } w.Header().Set("Link", urlStr) } enc := json.NewEncoder(w) if err := enc.Encode(catalogAPIResponse{ Repositories: repos[0:filled], }); err != nil { ch.Errors = append(ch.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } // Use the original URL from the request to create a new URL for // the link header func createLinkEntry(origURL string, maxEntries int, lastEntry string) (string, error) { calledURL, err := url.Parse(origURL) if err != nil { return "", err } v := url.Values{} v.Add("n", strconv.Itoa(maxEntries)) v.Add("last", lastEntry) calledURL.RawQuery = v.Encode() calledURL.Fragment = "" urlStr := fmt.Sprintf("<%s>; rel=\"next\"", calledURL.String()) return urlStr, nil } docker-registry-2.6.2~ds1/registry/handlers/context.go000066400000000000000000000105141313450123100231210ustar00rootroot00000000000000package handlers import ( "fmt" "net/http" "sync" "github.com/docker/distribution" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/auth" "golang.org/x/net/context" ) // Context should contain the request specific context for use in across // handlers. Resources that don't need to be shared across handlers should not // be on this object. type Context struct { // App points to the application structure that created this context. *App context.Context // Repository is the repository for the current request. All requests // should be scoped to a single repository. This field may be nil. Repository distribution.Repository // Errors is a collection of errors encountered during the request to be // returned to the client API. If errors are added to the collection, the // handler *must not* start the response via http.ResponseWriter. Errors errcode.Errors urlBuilder *v2.URLBuilder // TODO(stevvooe): The goal is too completely factor this context and // dispatching out of the web application. Ideally, we should lean on // context.Context for injection of these resources. } // Value overrides context.Context.Value to ensure that calls are routed to // correct context. func (ctx *Context) Value(key interface{}) interface{} { return ctx.Context.Value(key) } func getName(ctx context.Context) (name string) { return ctxu.GetStringValue(ctx, "vars.name") } func getReference(ctx context.Context) (reference string) { return ctxu.GetStringValue(ctx, "vars.reference") } var errDigestNotAvailable = fmt.Errorf("digest not available in context") func getDigest(ctx context.Context) (dgst digest.Digest, err error) { dgstStr := ctxu.GetStringValue(ctx, "vars.digest") if dgstStr == "" { ctxu.GetLogger(ctx).Errorf("digest not available") return "", errDigestNotAvailable } d, err := digest.ParseDigest(dgstStr) if err != nil { ctxu.GetLogger(ctx).Errorf("error parsing digest=%q: %v", dgstStr, err) return "", err } return d, nil } func getUploadUUID(ctx context.Context) (uuid string) { return ctxu.GetStringValue(ctx, "vars.uuid") } // getUserName attempts to resolve a username from the context and request. If // a username cannot be resolved, the empty string is returned. func getUserName(ctx context.Context, r *http.Request) string { username := ctxu.GetStringValue(ctx, auth.UserNameKey) // Fallback to request user with basic auth if username == "" { var ok bool uname, _, ok := basicAuth(r) if ok { username = uname } } return username } // contextManager allows us to associate net/context.Context instances with a // request, based on the memory identity of http.Request. This prepares http- // level context, which is not application specific. If this is called, // (*contextManager).release must be called on the context when the request is // completed. // // Providing this circumvents a lot of necessity for dispatchers with the // benefit of instantiating the request context much earlier. // // TODO(stevvooe): Consider making this facility a part of the context package. type contextManager struct { contexts map[*http.Request]context.Context mu sync.Mutex } // defaultContextManager is just a global instance to register request contexts. var defaultContextManager = newContextManager() func newContextManager() *contextManager { return &contextManager{ contexts: make(map[*http.Request]context.Context), } } // context either returns a new context or looks it up in the manager. func (cm *contextManager) context(parent context.Context, w http.ResponseWriter, r *http.Request) context.Context { cm.mu.Lock() defer cm.mu.Unlock() ctx, ok := cm.contexts[r] if ok { return ctx } if parent == nil { parent = ctxu.Background() } ctx = ctxu.WithRequest(parent, r) ctx, w = ctxu.WithResponseWriter(ctx, w) ctx = ctxu.WithLogger(ctx, ctxu.GetRequestLogger(ctx)) cm.contexts[r] = ctx return ctx } // releases frees any associated with resources from request. func (cm *contextManager) release(ctx context.Context) { cm.mu.Lock() defer cm.mu.Unlock() r, err := ctxu.GetRequest(ctx) if err != nil { ctxu.GetLogger(ctx).Errorf("no request found in context during release") return } delete(cm.contexts, r) } docker-registry-2.6.2~ds1/registry/handlers/health_test.go000066400000000000000000000112111313450123100237340ustar00rootroot00000000000000package handlers import ( "io/ioutil" "net" "net/http" "net/http/httptest" "os" "testing" "time" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" "github.com/docker/distribution/health" ) func TestFileHealthCheck(t *testing.T) { interval := time.Second tmpfile, err := ioutil.TempFile(os.TempDir(), "healthcheck") if err != nil { t.Fatalf("could not create temporary file: %v", err) } defer tmpfile.Close() config := &configuration.Configuration{ Storage: configuration.Storage{ "inmemory": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, Health: configuration.Health{ FileCheckers: []configuration.FileChecker{ { Interval: interval, File: tmpfile.Name(), }, }, }, } ctx := context.Background() app := NewApp(ctx, config) healthRegistry := health.NewRegistry() app.RegisterHealthChecks(healthRegistry) // Wait for health check to happen <-time.After(2 * interval) status := healthRegistry.CheckStatus() if len(status) != 1 { t.Fatal("expected 1 item in health check results") } if status[tmpfile.Name()] != "file exists" { t.Fatal(`did not get "file exists" result for health check`) } os.Remove(tmpfile.Name()) <-time.After(2 * interval) if len(healthRegistry.CheckStatus()) != 0 { t.Fatal("expected 0 items in health check results") } } func TestTCPHealthCheck(t *testing.T) { interval := time.Second ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("could not create listener: %v", err) } addrStr := ln.Addr().String() // Start accepting go func() { for { conn, err := ln.Accept() if err != nil { // listener was closed return } defer conn.Close() } }() config := &configuration.Configuration{ Storage: configuration.Storage{ "inmemory": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, Health: configuration.Health{ TCPCheckers: []configuration.TCPChecker{ { Interval: interval, Addr: addrStr, Timeout: 500 * time.Millisecond, }, }, }, } ctx := context.Background() app := NewApp(ctx, config) healthRegistry := health.NewRegistry() app.RegisterHealthChecks(healthRegistry) // Wait for health check to happen <-time.After(2 * interval) if len(healthRegistry.CheckStatus()) != 0 { t.Fatal("expected 0 items in health check results") } ln.Close() <-time.After(2 * interval) // Health check should now fail status := healthRegistry.CheckStatus() if len(status) != 1 { t.Fatal("expected 1 item in health check results") } if status[addrStr] != "connection to "+addrStr+" failed" { t.Fatal(`did not get "connection failed" result for health check`) } } func TestHTTPHealthCheck(t *testing.T) { interval := time.Second threshold := 3 stopFailing := make(chan struct{}) checkedServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != "HEAD" { t.Fatalf("expected HEAD request, got %s", r.Method) } select { case <-stopFailing: w.WriteHeader(http.StatusOK) default: w.WriteHeader(http.StatusInternalServerError) } })) config := &configuration.Configuration{ Storage: configuration.Storage{ "inmemory": configuration.Parameters{}, "maintenance": configuration.Parameters{"uploadpurging": map[interface{}]interface{}{ "enabled": false, }}, }, Health: configuration.Health{ HTTPCheckers: []configuration.HTTPChecker{ { Interval: interval, URI: checkedServer.URL, Threshold: threshold, }, }, }, } ctx := context.Background() app := NewApp(ctx, config) healthRegistry := health.NewRegistry() app.RegisterHealthChecks(healthRegistry) for i := 0; ; i++ { <-time.After(interval) status := healthRegistry.CheckStatus() if i < threshold-1 { // definitely shouldn't have hit the threshold yet if len(status) != 0 { t.Fatal("expected 1 item in health check results") } continue } if i < threshold+1 { // right on the threshold - don't expect a failure yet continue } if len(status) != 1 { t.Fatal("expected 1 item in health check results") } if status[checkedServer.URL] != "downstream service returned unexpected status: 500" { t.Fatal("did not get expected result for health check") } break } // Signal HTTP handler to start returning 200 close(stopFailing) <-time.After(2 * interval) if len(healthRegistry.CheckStatus()) != 0 { t.Fatal("expected 0 items in health check results") } } docker-registry-2.6.2~ds1/registry/handlers/helpers.go000066400000000000000000000045101313450123100230760ustar00rootroot00000000000000package handlers import ( "errors" "io" "net/http" ctxu "github.com/docker/distribution/context" ) // closeResources closes all the provided resources after running the target // handler. func closeResources(handler http.Handler, closers ...io.Closer) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { for _, closer := range closers { defer closer.Close() } handler.ServeHTTP(w, r) }) } // copyFullPayload copies the payload of an HTTP request to destWriter. If it // receives less content than expected, and the client disconnected during the // upload, it avoids sending a 400 error to keep the logs cleaner. // // The copy will be limited to `limit` bytes, if limit is greater than zero. func copyFullPayload(responseWriter http.ResponseWriter, r *http.Request, destWriter io.Writer, limit int64, context ctxu.Context, action string) error { // Get a channel that tells us if the client disconnects var clientClosed <-chan bool if notifier, ok := responseWriter.(http.CloseNotifier); ok { clientClosed = notifier.CloseNotify() } else { ctxu.GetLogger(context).Warnf("the ResponseWriter does not implement CloseNotifier (type: %T)", responseWriter) } var body = r.Body if limit > 0 { body = http.MaxBytesReader(responseWriter, body, limit) } // Read in the data, if any. copied, err := io.Copy(destWriter, body) if clientClosed != nil && (err != nil || (r.ContentLength > 0 && copied < r.ContentLength)) { // Didn't receive as much content as expected. Did the client // disconnect during the request? If so, avoid returning a 400 // error to keep the logs cleaner. select { case <-clientClosed: // Set the response code to "499 Client Closed Request" // Even though the connection has already been closed, // this causes the logger to pick up a 499 error // instead of showing 0 for the HTTP status. responseWriter.WriteHeader(499) ctxu.GetLoggerWithFields(context, map[interface{}]interface{}{ "error": err, "copied": copied, "contentLength": r.ContentLength, }, "error", "copied", "contentLength").Error("client disconnected during " + action) return errors.New("client disconnected") default: } } if err != nil { ctxu.GetLogger(context).Errorf("unknown error reading request payload: %v", err) return err } return nil } docker-registry-2.6.2~ds1/registry/handlers/hmac.go000066400000000000000000000034151313450123100223470ustar00rootroot00000000000000package handlers import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/json" "fmt" "time" ) // blobUploadState captures the state serializable state of the blob upload. type blobUploadState struct { // name is the primary repository under which the blob will be linked. Name string // UUID identifies the upload. UUID string // offset contains the current progress of the upload. Offset int64 // StartedAt is the original start time of the upload. StartedAt time.Time } type hmacKey string var errInvalidSecret = fmt.Errorf("invalid secret") // unpackUploadState unpacks and validates the blob upload state from the // token, using the hmacKey secret. func (secret hmacKey) unpackUploadState(token string) (blobUploadState, error) { var state blobUploadState tokenBytes, err := base64.URLEncoding.DecodeString(token) if err != nil { return state, err } mac := hmac.New(sha256.New, []byte(secret)) if len(tokenBytes) < mac.Size() { return state, errInvalidSecret } macBytes := tokenBytes[:mac.Size()] messageBytes := tokenBytes[mac.Size():] mac.Write(messageBytes) if !hmac.Equal(mac.Sum(nil), macBytes) { return state, errInvalidSecret } if err := json.Unmarshal(messageBytes, &state); err != nil { return state, err } return state, nil } // packUploadState packs the upload state signed with and hmac digest using // the hmacKey secret, encoding to url safe base64. The resulting token can be // used to share data with minimized risk of external tampering. func (secret hmacKey) packUploadState(lus blobUploadState) (string, error) { mac := hmac.New(sha256.New, []byte(secret)) p, err := json.Marshal(lus) if err != nil { return "", err } mac.Write(p) return base64.URLEncoding.EncodeToString(append(mac.Sum(nil), p...)), nil } docker-registry-2.6.2~ds1/registry/handlers/hmac_test.go000066400000000000000000000054671313450123100234170ustar00rootroot00000000000000package handlers import "testing" var blobUploadStates = []blobUploadState{ { Name: "hello", UUID: "abcd-1234-qwer-0987", Offset: 0, }, { Name: "hello-world", UUID: "abcd-1234-qwer-0987", Offset: 0, }, { Name: "h3ll0_w0rld", UUID: "abcd-1234-qwer-0987", Offset: 1337, }, { Name: "ABCDEFG", UUID: "ABCD-1234-QWER-0987", Offset: 1234567890, }, { Name: "this-is-A-sort-of-Long-name-for-Testing", UUID: "dead-1234-beef-0987", Offset: 8675309, }, } var secrets = []string{ "supersecret", "12345", "a", "SuperSecret", "Sup3r... S3cr3t!", "This is a reasonably long secret key that is used for the purpose of testing.", "\u2603+\u2744", // snowman+snowflake } // TestLayerUploadTokens constructs stateTokens from LayerUploadStates and // validates that the tokens can be used to reconstruct the proper upload state. func TestLayerUploadTokens(t *testing.T) { secret := hmacKey("supersecret") for _, testcase := range blobUploadStates { token, err := secret.packUploadState(testcase) if err != nil { t.Fatal(err) } lus, err := secret.unpackUploadState(token) if err != nil { t.Fatal(err) } assertBlobUploadStateEquals(t, testcase, lus) } } // TestHMACValidate ensures that any HMAC token providers are compatible if and // only if they share the same secret. func TestHMACValidation(t *testing.T) { for _, secret := range secrets { secret1 := hmacKey(secret) secret2 := hmacKey(secret) badSecret := hmacKey("DifferentSecret") for _, testcase := range blobUploadStates { token, err := secret1.packUploadState(testcase) if err != nil { t.Fatal(err) } lus, err := secret2.unpackUploadState(token) if err != nil { t.Fatal(err) } assertBlobUploadStateEquals(t, testcase, lus) _, err = badSecret.unpackUploadState(token) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", token) } badToken, err := badSecret.packUploadState(lus) if err != nil { t.Fatal(err) } _, err = secret1.unpackUploadState(badToken) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken) } _, err = secret2.unpackUploadState(badToken) if err == nil { t.Fatalf("Expected token provider to fail at retrieving state from token: %s", badToken) } } } } func assertBlobUploadStateEquals(t *testing.T, expected blobUploadState, received blobUploadState) { if expected.Name != received.Name { t.Fatalf("Expected Name=%q, Received Name=%q", expected.Name, received.Name) } if expected.UUID != received.UUID { t.Fatalf("Expected UUID=%q, Received UUID=%q", expected.UUID, received.UUID) } if expected.Offset != received.Offset { t.Fatalf("Expected Offset=%d, Received Offset=%d", expected.Offset, received.Offset) } } docker-registry-2.6.2~ds1/registry/handlers/hooks.go000066400000000000000000000021111313450123100225520ustar00rootroot00000000000000package handlers import ( "bytes" "errors" "fmt" "strings" "text/template" "github.com/Sirupsen/logrus" ) // logHook is for hooking Panic in web application type logHook struct { LevelsParam []string Mail *mailer } // Fire forwards an error to LogHook func (hook *logHook) Fire(entry *logrus.Entry) error { addr := strings.Split(hook.Mail.Addr, ":") if len(addr) != 2 { return errors.New("Invalid Mail Address") } host := addr[0] subject := fmt.Sprintf("[%s] %s: %s", entry.Level, host, entry.Message) html := ` {{.Message}} {{range $key, $value := .Data}} {{$key}}: {{$value}} {{end}} ` b := bytes.NewBuffer(make([]byte, 0)) t := template.Must(template.New("mail body").Parse(html)) if err := t.Execute(b, entry); err != nil { return err } body := fmt.Sprintf("%s", b) return hook.Mail.sendMail(subject, body) } // Levels contains hook levels to be catched func (hook *logHook) Levels() []logrus.Level { levels := []logrus.Level{} for _, v := range hook.LevelsParam { lv, _ := logrus.ParseLevel(v) levels = append(levels, lv) } return levels } docker-registry-2.6.2~ds1/registry/handlers/images.go000066400000000000000000000337261313450123100227140ustar00rootroot00000000000000package handlers import ( "bytes" "fmt" "net/http" "strings" "github.com/docker/distribution" ctxu "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/docker/distribution/registry/auth" "github.com/gorilla/handlers" ) // These constants determine which architecture and OS to choose from a // manifest list when downconverting it to a schema1 manifest. const ( defaultArch = "amd64" defaultOS = "linux" maxManifestBodySize = 4 << 20 ) // imageManifestDispatcher takes the request context and builds the // appropriate handler for handling image manifest requests. func imageManifestDispatcher(ctx *Context, r *http.Request) http.Handler { imageManifestHandler := &imageManifestHandler{ Context: ctx, } reference := getReference(ctx) dgst, err := digest.ParseDigest(reference) if err != nil { // We just have a tag imageManifestHandler.Tag = reference } else { imageManifestHandler.Digest = dgst } mhandler := handlers.MethodHandler{ "GET": http.HandlerFunc(imageManifestHandler.GetImageManifest), "HEAD": http.HandlerFunc(imageManifestHandler.GetImageManifest), } if !ctx.readOnly { mhandler["PUT"] = http.HandlerFunc(imageManifestHandler.PutImageManifest) mhandler["DELETE"] = http.HandlerFunc(imageManifestHandler.DeleteImageManifest) } return mhandler } // imageManifestHandler handles http operations on image manifests. type imageManifestHandler struct { *Context // One of tag or digest gets set, depending on what is present in context. Tag string Digest digest.Digest } // GetImageManifest fetches the image manifest from the storage backend, if it exists. func (imh *imageManifestHandler) GetImageManifest(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(imh).Debug("GetImageManifest") manifests, err := imh.Repository.Manifests(imh) if err != nil { imh.Errors = append(imh.Errors, err) return } var manifest distribution.Manifest if imh.Tag != "" { tags := imh.Repository.Tags(imh) desc, err := tags.Get(imh, imh.Tag) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) return } imh.Digest = desc.Digest } if etagMatch(r, imh.Digest.String()) { w.WriteHeader(http.StatusNotModified) return } var options []distribution.ManifestServiceOption if imh.Tag != "" { options = append(options, distribution.WithTag(imh.Tag)) } manifest, err = manifests.Get(imh, imh.Digest, options...) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) return } supportsSchema2 := false supportsManifestList := false // this parsing of Accept headers is not quite as full-featured as godoc.org's parser, but we don't care about "q=" values // https://github.com/golang/gddo/blob/e91d4165076d7474d20abda83f92d15c7ebc3e81/httputil/header/header.go#L165-L202 for _, acceptHeader := range r.Header["Accept"] { // r.Header[...] is a slice in case the request contains the same header more than once // if the header isn't set, we'll get the zero value, which "range" will handle gracefully // we need to split each header value on "," to get the full list of "Accept" values (per RFC 2616) // https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 for _, mediaType := range strings.Split(acceptHeader, ",") { // remove "; q=..." if present if i := strings.Index(mediaType, ";"); i >= 0 { mediaType = mediaType[:i] } // it's common (but not required) for Accept values to be space separated ("a/b, c/d, e/f") mediaType = strings.TrimSpace(mediaType) if mediaType == schema2.MediaTypeManifest { supportsSchema2 = true } if mediaType == manifestlist.MediaTypeManifestList { supportsManifestList = true } } } schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest) manifestList, isManifestList := manifest.(*manifestlist.DeserializedManifestList) // Only rewrite schema2 manifests when they are being fetched by tag. // If they are being fetched by digest, we can't return something not // matching the digest. if imh.Tag != "" && isSchema2 && !supportsSchema2 { // Rewrite manifest in schema1 format ctxu.GetLogger(imh).Infof("rewriting manifest %s in schema1 format to support old client", imh.Digest.String()) manifest, err = imh.convertSchema2Manifest(schema2Manifest) if err != nil { return } } else if imh.Tag != "" && isManifestList && !supportsManifestList { // Rewrite manifest in schema1 format ctxu.GetLogger(imh).Infof("rewriting manifest list %s in schema1 format to support old client", imh.Digest.String()) // Find the image manifest corresponding to the default // platform var manifestDigest digest.Digest for _, manifestDescriptor := range manifestList.Manifests { if manifestDescriptor.Platform.Architecture == defaultArch && manifestDescriptor.Platform.OS == defaultOS { manifestDigest = manifestDescriptor.Digest break } } if manifestDigest == "" { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown) return } manifest, err = manifests.Get(imh, manifestDigest) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown.WithDetail(err)) return } // If necessary, convert the image manifest if schema2Manifest, isSchema2 := manifest.(*schema2.DeserializedManifest); isSchema2 && !supportsSchema2 { manifest, err = imh.convertSchema2Manifest(schema2Manifest) if err != nil { return } } } ct, p, err := manifest.Payload() if err != nil { return } w.Header().Set("Content-Type", ct) w.Header().Set("Content-Length", fmt.Sprint(len(p))) w.Header().Set("Docker-Content-Digest", imh.Digest.String()) w.Header().Set("Etag", fmt.Sprintf(`"%s"`, imh.Digest)) w.Write(p) } func (imh *imageManifestHandler) convertSchema2Manifest(schema2Manifest *schema2.DeserializedManifest) (distribution.Manifest, error) { targetDescriptor := schema2Manifest.Target() blobs := imh.Repository.Blobs(imh) configJSON, err := blobs.Get(imh, targetDescriptor.Digest) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) return nil, err } ref := imh.Repository.Named() if imh.Tag != "" { ref, err = reference.WithTag(ref, imh.Tag) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail(err)) return nil, err } } builder := schema1.NewConfigManifestBuilder(imh.Repository.Blobs(imh), imh.Context.App.trustKey, ref, configJSON) for _, d := range schema2Manifest.Layers { if err := builder.AppendReference(d); err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) return nil, err } } manifest, err := builder.Build(imh) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) return nil, err } imh.Digest = digest.FromBytes(manifest.(*schema1.SignedManifest).Canonical) return manifest, nil } func etagMatch(r *http.Request, etag string) bool { for _, headerVal := range r.Header["If-None-Match"] { if headerVal == etag || headerVal == fmt.Sprintf(`"%s"`, etag) { // allow quoted or unquoted return true } } return false } // PutImageManifest validates and stores an image in the registry. func (imh *imageManifestHandler) PutImageManifest(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(imh).Debug("PutImageManifest") manifests, err := imh.Repository.Manifests(imh) if err != nil { imh.Errors = append(imh.Errors, err) return } var jsonBuf bytes.Buffer if err := copyFullPayload(w, r, &jsonBuf, maxManifestBodySize, imh, "image manifest PUT"); err != nil { // copyFullPayload reports the error if necessary imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err.Error())) return } mediaType := r.Header.Get("Content-Type") manifest, desc, err := distribution.UnmarshalManifest(mediaType, jsonBuf.Bytes()) if err != nil { imh.Errors = append(imh.Errors, v2.ErrorCodeManifestInvalid.WithDetail(err)) return } if imh.Digest != "" { if desc.Digest != imh.Digest { ctxu.GetLogger(imh).Errorf("payload digest does match: %q != %q", desc.Digest, imh.Digest) imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) return } } else if imh.Tag != "" { imh.Digest = desc.Digest } else { imh.Errors = append(imh.Errors, v2.ErrorCodeTagInvalid.WithDetail("no tag or digest specified")) return } var options []distribution.ManifestServiceOption if imh.Tag != "" { options = append(options, distribution.WithTag(imh.Tag)) } if err := imh.applyResourcePolicy(manifest); err != nil { imh.Errors = append(imh.Errors, err) return } _, err = manifests.Put(imh, manifest, options...) if err != nil { // TODO(stevvooe): These error handling switches really need to be // handled by an app global mapper. if err == distribution.ErrUnsupported { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) return } if err == distribution.ErrAccessDenied { imh.Errors = append(imh.Errors, errcode.ErrorCodeDenied) return } switch err := err.(type) { case distribution.ErrManifestVerification: for _, verificationError := range err { switch verificationError := verificationError.(type) { case distribution.ErrManifestBlobUnknown: imh.Errors = append(imh.Errors, v2.ErrorCodeManifestBlobUnknown.WithDetail(verificationError.Digest)) case distribution.ErrManifestNameInvalid: imh.Errors = append(imh.Errors, v2.ErrorCodeNameInvalid.WithDetail(err)) case distribution.ErrManifestUnverified: imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnverified) default: if verificationError == digest.ErrDigestInvalidFormat { imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) } else { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown, verificationError) } } } case errcode.Error: imh.Errors = append(imh.Errors, err) default: imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } return } // Tag this manifest if imh.Tag != "" { tags := imh.Repository.Tags(imh) err = tags.Tag(imh, imh.Tag, desc) if err != nil { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } // Construct a canonical url for the uploaded manifest. ref, err := reference.WithDigest(imh.Repository.Named(), imh.Digest) if err != nil { imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } location, err := imh.urlBuilder.BuildManifestURL(ref) if err != nil { // NOTE(stevvooe): Given the behavior above, this absurdly unlikely to // happen. We'll log the error here but proceed as if it worked. Worst // case, we set an empty location header. ctxu.GetLogger(imh).Errorf("error building manifest url from digest: %v", err) } w.Header().Set("Location", location) w.Header().Set("Docker-Content-Digest", imh.Digest.String()) w.WriteHeader(http.StatusCreated) } // applyResourcePolicy checks whether the resource class matches what has // been authorized and allowed by the policy configuration. func (imh *imageManifestHandler) applyResourcePolicy(manifest distribution.Manifest) error { allowedClasses := imh.App.Config.Policy.Repository.Classes if len(allowedClasses) == 0 { return nil } var class string switch m := manifest.(type) { case *schema1.SignedManifest: class = "image" case *schema2.DeserializedManifest: switch m.Config.MediaType { case schema2.MediaTypeConfig: class = "image" case schema2.MediaTypePluginConfig: class = "plugin" default: message := fmt.Sprintf("unknown manifest class for %s", m.Config.MediaType) return errcode.ErrorCodeDenied.WithMessage(message) } } if class == "" { return nil } // Check to see if class is allowed in registry var allowedClass bool for _, c := range allowedClasses { if class == c { allowedClass = true break } } if !allowedClass { message := fmt.Sprintf("registry does not allow %s manifest", class) return errcode.ErrorCodeDenied.WithMessage(message) } resources := auth.AuthorizedResources(imh) n := imh.Repository.Named().Name() var foundResource bool for _, r := range resources { if r.Name == n { if r.Class == "" { r.Class = "image" } if r.Class == class { return nil } foundResource = true } } // resource was found but no matching class was found if foundResource { message := fmt.Sprintf("repository not authorized for %s manifest", class) return errcode.ErrorCodeDenied.WithMessage(message) } return nil } // DeleteImageManifest removes the manifest with the given digest from the registry. func (imh *imageManifestHandler) DeleteImageManifest(w http.ResponseWriter, r *http.Request) { ctxu.GetLogger(imh).Debug("DeleteImageManifest") manifests, err := imh.Repository.Manifests(imh) if err != nil { imh.Errors = append(imh.Errors, err) return } err = manifests.Delete(imh, imh.Digest) if err != nil { switch err { case digest.ErrDigestUnsupported: case digest.ErrDigestInvalidFormat: imh.Errors = append(imh.Errors, v2.ErrorCodeDigestInvalid) return case distribution.ErrBlobUnknown: imh.Errors = append(imh.Errors, v2.ErrorCodeManifestUnknown) return case distribution.ErrUnsupported: imh.Errors = append(imh.Errors, errcode.ErrorCodeUnsupported) return default: imh.Errors = append(imh.Errors, errcode.ErrorCodeUnknown) return } } tagService := imh.Repository.Tags(imh) referencedTags, err := tagService.Lookup(imh, distribution.Descriptor{Digest: imh.Digest}) if err != nil { imh.Errors = append(imh.Errors, err) return } for _, tag := range referencedTags { if err := tagService.Untag(imh, tag); err != nil { imh.Errors = append(imh.Errors, err) return } } w.WriteHeader(http.StatusAccepted) } docker-registry-2.6.2~ds1/registry/handlers/mail.go000066400000000000000000000016231313450123100223600ustar00rootroot00000000000000package handlers import ( "errors" "net/smtp" "strings" ) // mailer provides fields of email configuration for sending. type mailer struct { Addr, Username, Password, From string Insecure bool To []string } // sendMail allows users to send email, only if mail parameters is configured correctly. func (mail *mailer) sendMail(subject, message string) error { addr := strings.Split(mail.Addr, ":") if len(addr) != 2 { return errors.New("Invalid Mail Address") } host := addr[0] msg := []byte("To:" + strings.Join(mail.To, ";") + "\r\nFrom: " + mail.From + "\r\nSubject: " + subject + "\r\nContent-Type: text/plain\r\n\r\n" + message) auth := smtp.PlainAuth( "", mail.Username, mail.Password, host, ) err := smtp.SendMail( mail.Addr, auth, mail.From, mail.To, []byte(msg), ) if err != nil { return err } return nil } docker-registry-2.6.2~ds1/registry/handlers/tags.go000066400000000000000000000030771313450123100224010ustar00rootroot00000000000000package handlers import ( "encoding/json" "net/http" "github.com/docker/distribution" "github.com/docker/distribution/registry/api/errcode" "github.com/docker/distribution/registry/api/v2" "github.com/gorilla/handlers" ) // tagsDispatcher constructs the tags handler api endpoint. func tagsDispatcher(ctx *Context, r *http.Request) http.Handler { tagsHandler := &tagsHandler{ Context: ctx, } return handlers.MethodHandler{ "GET": http.HandlerFunc(tagsHandler.GetTags), } } // tagsHandler handles requests for lists of tags under a repository name. type tagsHandler struct { *Context } type tagsAPIResponse struct { Name string `json:"name"` Tags []string `json:"tags"` } // GetTags returns a json list of tags for a specific image name. func (th *tagsHandler) GetTags(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() tagService := th.Repository.Tags(th) tags, err := tagService.All(th) if err != nil { switch err := err.(type) { case distribution.ErrRepositoryUnknown: th.Errors = append(th.Errors, v2.ErrorCodeNameUnknown.WithDetail(map[string]string{"name": th.Repository.Named().Name()})) case errcode.Error: th.Errors = append(th.Errors, err) default: th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) } return } w.Header().Set("Content-Type", "application/json; charset=utf-8") enc := json.NewEncoder(w) if err := enc.Encode(tagsAPIResponse{ Name: th.Repository.Named().Name(), Tags: tags, }); err != nil { th.Errors = append(th.Errors, errcode.ErrorCodeUnknown.WithDetail(err)) return } } docker-registry-2.6.2~ds1/registry/listener/000077500000000000000000000000001313450123100211325ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/listener/listener.go000066400000000000000000000032501313450123100233060ustar00rootroot00000000000000package listener import ( "fmt" "net" "os" "time" ) // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted // connections. It's used by ListenAndServe and ListenAndServeTLS so // dead TCP connections (e.g. closing laptop mid-download) eventually // go away. // it is a plain copy-paste from net/http/server.go type tcpKeepAliveListener struct { *net.TCPListener } func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { tc, err := ln.AcceptTCP() if err != nil { return } tc.SetKeepAlive(true) tc.SetKeepAlivePeriod(3 * time.Minute) return tc, nil } // NewListener announces on laddr and net. Accepted values of the net are // 'unix' and 'tcp' func NewListener(net, laddr string) (net.Listener, error) { switch net { case "unix": return newUnixListener(laddr) case "tcp", "": // an empty net means tcp return newTCPListener(laddr) default: return nil, fmt.Errorf("unknown address type %s", net) } } func newUnixListener(laddr string) (net.Listener, error) { fi, err := os.Stat(laddr) if err == nil { // the file exists. // try to remove it if it's a socket if !isSocket(fi.Mode()) { return nil, fmt.Errorf("file %s exists and is not a socket", laddr) } if err := os.Remove(laddr); err != nil { return nil, err } } else if !os.IsNotExist(err) { // we can't do stat on the file. // it means we can not remove it return nil, err } return net.Listen("unix", laddr) } func isSocket(m os.FileMode) bool { return m&os.ModeSocket != 0 } func newTCPListener(laddr string) (net.Listener, error) { ln, err := net.Listen("tcp", laddr) if err != nil { return nil, err } return tcpKeepAliveListener{ln.(*net.TCPListener)}, nil } docker-registry-2.6.2~ds1/registry/middleware/000077500000000000000000000000001313450123100214225ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/middleware/registry/000077500000000000000000000000001313450123100232725ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/middleware/registry/middleware.go000066400000000000000000000033321313450123100257370ustar00rootroot00000000000000package middleware import ( "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage" ) // InitFunc is the type of a RegistryMiddleware factory function and is // used to register the constructor for different RegistryMiddleware backends. type InitFunc func(ctx context.Context, registry distribution.Namespace, options map[string]interface{}) (distribution.Namespace, error) var middlewares map[string]InitFunc var registryoptions []storage.RegistryOption // Register is used to register an InitFunc for // a RegistryMiddleware backend with the given name. func Register(name string, initFunc InitFunc) error { if middlewares == nil { middlewares = make(map[string]InitFunc) } if _, exists := middlewares[name]; exists { return fmt.Errorf("name already registered: %s", name) } middlewares[name] = initFunc return nil } // Get constructs a RegistryMiddleware with the given options using the named backend. func Get(ctx context.Context, name string, options map[string]interface{}, registry distribution.Namespace) (distribution.Namespace, error) { if middlewares != nil { if initFunc, exists := middlewares[name]; exists { return initFunc(ctx, registry, options) } } return nil, fmt.Errorf("no registry middleware registered with name: %s", name) } // RegisterOptions adds more options to RegistryOption list. Options get applied before // any other configuration-based options. func RegisterOptions(options ...storage.RegistryOption) error { registryoptions = append(registryoptions, options...) return nil } // GetRegistryOptions returns list of RegistryOption. func GetRegistryOptions() []storage.RegistryOption { return registryoptions } docker-registry-2.6.2~ds1/registry/middleware/repository/000077500000000000000000000000001313450123100236415ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/middleware/repository/middleware.go000066400000000000000000000024001313450123100263010ustar00rootroot00000000000000package middleware import ( "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" ) // InitFunc is the type of a RepositoryMiddleware factory function and is // used to register the constructor for different RepositoryMiddleware backends. type InitFunc func(ctx context.Context, repository distribution.Repository, options map[string]interface{}) (distribution.Repository, error) var middlewares map[string]InitFunc // Register is used to register an InitFunc for // a RepositoryMiddleware backend with the given name. func Register(name string, initFunc InitFunc) error { if middlewares == nil { middlewares = make(map[string]InitFunc) } if _, exists := middlewares[name]; exists { return fmt.Errorf("name already registered: %s", name) } middlewares[name] = initFunc return nil } // Get constructs a RepositoryMiddleware with the given options using the named backend. func Get(ctx context.Context, name string, options map[string]interface{}, repository distribution.Repository) (distribution.Repository, error) { if middlewares != nil { if initFunc, exists := middlewares[name]; exists { return initFunc(ctx, repository, options) } } return nil, fmt.Errorf("no repository middleware registered with name: %s", name) } docker-registry-2.6.2~ds1/registry/proxy/000077500000000000000000000000001313450123100204665ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/proxy/proxyauth.go000066400000000000000000000034341313450123100230640ustar00rootroot00000000000000package proxy import ( "net/http" "net/url" "strings" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" ) const challengeHeader = "Docker-Distribution-Api-Version" type userpass struct { username string password string } type credentials struct { creds map[string]userpass } func (c credentials) Basic(u *url.URL) (string, string) { up := c.creds[u.String()] return up.username, up.password } func (c credentials) RefreshToken(u *url.URL, service string) string { return "" } func (c credentials) SetRefreshToken(u *url.URL, service, token string) { } // configureAuth stores credentials for challenge responses func configureAuth(username, password, remoteURL string) (auth.CredentialStore, error) { creds := map[string]userpass{} authURLs, err := getAuthURLs(remoteURL) if err != nil { return nil, err } for _, url := range authURLs { context.GetLogger(context.Background()).Infof("Discovered token authentication URL: %s", url) creds[url] = userpass{ username: username, password: password, } } return credentials{creds: creds}, nil } func getAuthURLs(remoteURL string) ([]string, error) { authURLs := []string{} resp, err := http.Get(remoteURL + "/v2/") if err != nil { return nil, err } defer resp.Body.Close() for _, c := range challenge.ResponseChallenges(resp) { if strings.EqualFold(c.Scheme, "bearer") { authURLs = append(authURLs, c.Parameters["realm"]) } } return authURLs, nil } func ping(manager challenge.Manager, endpoint, versionHeader string) error { resp, err := http.Get(endpoint) if err != nil { return err } defer resp.Body.Close() if err := manager.AddResponse(resp); err != nil { return err } return nil } docker-registry-2.6.2~ds1/registry/proxy/proxyblobstore.go000066400000000000000000000131761313450123100241220ustar00rootroot00000000000000package proxy import ( "io" "net/http" "strconv" "sync" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/proxy/scheduler" ) // todo(richardscothern): from cache control header or config file const blobTTL = time.Duration(24 * 7 * time.Hour) type proxyBlobStore struct { localStore distribution.BlobStore remoteStore distribution.BlobService scheduler *scheduler.TTLExpirationScheduler repositoryName reference.Named authChallenger authChallenger } var _ distribution.BlobStore = &proxyBlobStore{} // inflight tracks currently downloading blobs var inflight = make(map[digest.Digest]struct{}) // mu protects inflight var mu sync.Mutex func setResponseHeaders(w http.ResponseWriter, length int64, mediaType string, digest digest.Digest) { w.Header().Set("Content-Length", strconv.FormatInt(length, 10)) w.Header().Set("Content-Type", mediaType) w.Header().Set("Docker-Content-Digest", digest.String()) w.Header().Set("Etag", digest.String()) } func (pbs *proxyBlobStore) copyContent(ctx context.Context, dgst digest.Digest, writer io.Writer) (distribution.Descriptor, error) { desc, err := pbs.remoteStore.Stat(ctx, dgst) if err != nil { return distribution.Descriptor{}, err } if w, ok := writer.(http.ResponseWriter); ok { setResponseHeaders(w, desc.Size, desc.MediaType, dgst) } remoteReader, err := pbs.remoteStore.Open(ctx, dgst) if err != nil { return distribution.Descriptor{}, err } defer remoteReader.Close() _, err = io.CopyN(writer, remoteReader, desc.Size) if err != nil { return distribution.Descriptor{}, err } proxyMetrics.BlobPush(uint64(desc.Size)) return desc, nil } func (pbs *proxyBlobStore) serveLocal(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) (bool, error) { localDesc, err := pbs.localStore.Stat(ctx, dgst) if err != nil { // Stat can report a zero sized file here if it's checked between creation // and population. Return nil error, and continue return false, nil } if err == nil { proxyMetrics.BlobPush(uint64(localDesc.Size)) return true, pbs.localStore.ServeBlob(ctx, w, r, dgst) } return false, nil } func (pbs *proxyBlobStore) storeLocal(ctx context.Context, dgst digest.Digest) error { defer func() { mu.Lock() delete(inflight, dgst) mu.Unlock() }() var desc distribution.Descriptor var err error var bw distribution.BlobWriter bw, err = pbs.localStore.Create(ctx) if err != nil { return err } desc, err = pbs.copyContent(ctx, dgst, bw) if err != nil { return err } _, err = bw.Commit(ctx, desc) if err != nil { return err } return nil } func (pbs *proxyBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { served, err := pbs.serveLocal(ctx, w, r, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error serving blob from local storage: %s", err.Error()) return err } if served { return nil } if err := pbs.authChallenger.tryEstablishChallenges(ctx); err != nil { return err } mu.Lock() _, ok := inflight[dgst] if ok { mu.Unlock() _, err := pbs.copyContent(ctx, dgst, w) return err } inflight[dgst] = struct{}{} mu.Unlock() go func(dgst digest.Digest) { if err := pbs.storeLocal(ctx, dgst); err != nil { context.GetLogger(ctx).Errorf("Error committing to storage: %s", err.Error()) } blobRef, err := reference.WithDigest(pbs.repositoryName, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error creating reference: %s", err) return } pbs.scheduler.AddBlob(blobRef, repositoryTTL) }(dgst) _, err = pbs.copyContent(ctx, dgst, w) if err != nil { return err } return nil } func (pbs *proxyBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { desc, err := pbs.localStore.Stat(ctx, dgst) if err == nil { return desc, err } if err != distribution.ErrBlobUnknown { return distribution.Descriptor{}, err } if err := pbs.authChallenger.tryEstablishChallenges(ctx); err != nil { return distribution.Descriptor{}, err } return pbs.remoteStore.Stat(ctx, dgst) } func (pbs *proxyBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { blob, err := pbs.localStore.Get(ctx, dgst) if err == nil { return blob, nil } if err := pbs.authChallenger.tryEstablishChallenges(ctx); err != nil { return []byte{}, err } blob, err = pbs.remoteStore.Get(ctx, dgst) if err != nil { return []byte{}, err } _, err = pbs.localStore.Put(ctx, "", blob) if err != nil { return []byte{}, err } return blob, nil } // Unsupported functions func (pbs *proxyBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { return distribution.Descriptor{}, distribution.ErrUnsupported } func (pbs *proxyBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { return nil, distribution.ErrUnsupported } func (pbs *proxyBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { return nil, distribution.ErrUnsupported } func (pbs *proxyBlobStore) Mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest) (distribution.Descriptor, error) { return distribution.Descriptor{}, distribution.ErrUnsupported } func (pbs *proxyBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { return nil, distribution.ErrUnsupported } func (pbs *proxyBlobStore) Delete(ctx context.Context, dgst digest.Digest) error { return distribution.ErrUnsupported } docker-registry-2.6.2~ds1/registry/proxy/proxyblobstore_test.go000066400000000000000000000233031313450123100251520ustar00rootroot00000000000000package proxy import ( "io/ioutil" "math/rand" "net/http" "net/http/httptest" "sync" "testing" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/filesystem" "github.com/docker/distribution/registry/storage/driver/inmemory" ) var sbsMu sync.Mutex type statsBlobStore struct { stats map[string]int blobs distribution.BlobStore } func (sbs statsBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { sbsMu.Lock() sbs.stats["put"]++ sbsMu.Unlock() return sbs.blobs.Put(ctx, mediaType, p) } func (sbs statsBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { sbsMu.Lock() sbs.stats["get"]++ sbsMu.Unlock() return sbs.blobs.Get(ctx, dgst) } func (sbs statsBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { sbsMu.Lock() sbs.stats["create"]++ sbsMu.Unlock() return sbs.blobs.Create(ctx, options...) } func (sbs statsBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { sbsMu.Lock() sbs.stats["resume"]++ sbsMu.Unlock() return sbs.blobs.Resume(ctx, id) } func (sbs statsBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { sbsMu.Lock() sbs.stats["open"]++ sbsMu.Unlock() return sbs.blobs.Open(ctx, dgst) } func (sbs statsBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { sbsMu.Lock() sbs.stats["serveblob"]++ sbsMu.Unlock() return sbs.blobs.ServeBlob(ctx, w, r, dgst) } func (sbs statsBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { sbsMu.Lock() sbs.stats["stat"]++ sbsMu.Unlock() return sbs.blobs.Stat(ctx, dgst) } func (sbs statsBlobStore) Delete(ctx context.Context, dgst digest.Digest) error { sbsMu.Lock() sbs.stats["delete"]++ sbsMu.Unlock() return sbs.blobs.Delete(ctx, dgst) } type testEnv struct { numUnique int inRemote []distribution.Descriptor store proxyBlobStore ctx context.Context } func (te *testEnv) LocalStats() *map[string]int { sbsMu.Lock() ls := te.store.localStore.(statsBlobStore).stats sbsMu.Unlock() return &ls } func (te *testEnv) RemoteStats() *map[string]int { sbsMu.Lock() rs := te.store.remoteStore.(statsBlobStore).stats sbsMu.Unlock() return &rs } // Populate remote store and record the digests func makeTestEnv(t *testing.T, name string) *testEnv { nameRef, err := reference.ParseNamed(name) if err != nil { t.Fatalf("unable to parse reference: %s", err) } ctx := context.Background() truthDir, err := ioutil.TempDir("", "truth") if err != nil { t.Fatalf("unable to create tempdir: %s", err) } cacheDir, err := ioutil.TempDir("", "cache") if err != nil { t.Fatalf("unable to create tempdir: %s", err) } localDriver, err := filesystem.FromParameters(map[string]interface{}{ "rootdirectory": truthDir, }) if err != nil { t.Fatalf("unable to create filesystem driver: %s", err) } // todo: create a tempfile area here localRegistry, err := storage.NewRegistry(ctx, localDriver, storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableRedirect, storage.DisableDigestResumption) if err != nil { t.Fatalf("error creating registry: %v", err) } localRepo, err := localRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } cacheDriver, err := filesystem.FromParameters(map[string]interface{}{ "rootdirectory": cacheDir, }) if err != nil { t.Fatalf("unable to create filesystem driver: %s", err) } truthRegistry, err := storage.NewRegistry(ctx, cacheDriver, storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider())) if err != nil { t.Fatalf("error creating registry: %v", err) } truthRepo, err := truthRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } truthBlobs := statsBlobStore{ stats: make(map[string]int), blobs: truthRepo.Blobs(ctx), } localBlobs := statsBlobStore{ stats: make(map[string]int), blobs: localRepo.Blobs(ctx), } s := scheduler.New(ctx, inmemory.New(), "/scheduler-state.json") proxyBlobStore := proxyBlobStore{ repositoryName: nameRef, remoteStore: truthBlobs, localStore: localBlobs, scheduler: s, authChallenger: &mockChallenger{}, } te := &testEnv{ store: proxyBlobStore, ctx: ctx, } return te } func makeBlob(size int) []byte { blob := make([]byte, size, size) for i := 0; i < size; i++ { blob[i] = byte('A' + rand.Int()%48) } return blob } func init() { rand.Seed(42) } func perm(m []distribution.Descriptor) []distribution.Descriptor { for i := 0; i < len(m); i++ { j := rand.Intn(i + 1) tmp := m[i] m[i] = m[j] m[j] = tmp } return m } func populate(t *testing.T, te *testEnv, blobCount, size, numUnique int) { var inRemote []distribution.Descriptor for i := 0; i < numUnique; i++ { bytes := makeBlob(size) for j := 0; j < blobCount/numUnique; j++ { desc, err := te.store.remoteStore.Put(te.ctx, "", bytes) if err != nil { t.Fatalf("Put in store") } inRemote = append(inRemote, desc) } } te.inRemote = inRemote te.numUnique = numUnique } func TestProxyStoreGet(t *testing.T) { te := makeTestEnv(t, "foo/bar") localStats := te.LocalStats() remoteStats := te.RemoteStats() populate(t, te, 1, 10, 1) _, err := te.store.Get(te.ctx, te.inRemote[0].Digest) if err != nil { t.Fatal(err) } if (*localStats)["get"] != 1 && (*localStats)["put"] != 1 { t.Errorf("Unexpected local counts") } if (*remoteStats)["get"] != 1 { t.Errorf("Unexpected remote get count") } _, err = te.store.Get(te.ctx, te.inRemote[0].Digest) if err != nil { t.Fatal(err) } if (*localStats)["get"] != 2 && (*localStats)["put"] != 1 { t.Errorf("Unexpected local counts") } if (*remoteStats)["get"] != 1 { t.Errorf("Unexpected remote get count") } } func TestProxyStoreStat(t *testing.T) { te := makeTestEnv(t, "foo/bar") remoteBlobCount := 1 populate(t, te, remoteBlobCount, 10, 1) localStats := te.LocalStats() remoteStats := te.RemoteStats() // Stat - touches both stores for _, d := range te.inRemote { _, err := te.store.Stat(te.ctx, d.Digest) if err != nil { t.Fatalf("Error stating proxy store") } } if (*localStats)["stat"] != remoteBlobCount { t.Errorf("Unexpected local stat count") } if (*remoteStats)["stat"] != remoteBlobCount { t.Errorf("Unexpected remote stat count") } if te.store.authChallenger.(*mockChallenger).count != len(te.inRemote) { t.Fatalf("Unexpected auth challenge count, got %#v", te.store.authChallenger) } } func TestProxyStoreServeHighConcurrency(t *testing.T) { te := makeTestEnv(t, "foo/bar") blobSize := 200 blobCount := 10 numUnique := 1 populate(t, te, blobCount, blobSize, numUnique) numClients := 16 testProxyStoreServe(t, te, numClients) } func TestProxyStoreServeMany(t *testing.T) { te := makeTestEnv(t, "foo/bar") blobSize := 200 blobCount := 10 numUnique := 4 populate(t, te, blobCount, blobSize, numUnique) numClients := 4 testProxyStoreServe(t, te, numClients) } // todo(richardscothern): blobCount must be smaller than num clients func TestProxyStoreServeBig(t *testing.T) { te := makeTestEnv(t, "foo/bar") blobSize := 2 << 20 blobCount := 4 numUnique := 2 populate(t, te, blobCount, blobSize, numUnique) numClients := 4 testProxyStoreServe(t, te, numClients) } // testProxyStoreServe will create clients to consume all blobs // populated in the truth store func testProxyStoreServe(t *testing.T, te *testEnv, numClients int) { localStats := te.LocalStats() remoteStats := te.RemoteStats() var wg sync.WaitGroup for i := 0; i < numClients; i++ { // Serveblob - pulls through blobs wg.Add(1) go func() { defer wg.Done() for _, remoteBlob := range te.inRemote { w := httptest.NewRecorder() r, err := http.NewRequest("GET", "", nil) if err != nil { t.Fatal(err) } err = te.store.ServeBlob(te.ctx, w, r, remoteBlob.Digest) if err != nil { t.Fatalf(err.Error()) } bodyBytes := w.Body.Bytes() localDigest := digest.FromBytes(bodyBytes) if localDigest != remoteBlob.Digest { t.Fatalf("Mismatching blob fetch from proxy") } } }() } wg.Wait() remoteBlobCount := len(te.inRemote) sbsMu.Lock() if (*localStats)["stat"] != remoteBlobCount*numClients && (*localStats)["create"] != te.numUnique { sbsMu.Unlock() t.Fatal("Expected: stat:", remoteBlobCount*numClients, "create:", remoteBlobCount) } sbsMu.Unlock() // Wait for any async storage goroutines to finish time.Sleep(3 * time.Second) sbsMu.Lock() remoteStatCount := (*remoteStats)["stat"] remoteOpenCount := (*remoteStats)["open"] sbsMu.Unlock() // Serveblob - blobs come from local for _, dr := range te.inRemote { w := httptest.NewRecorder() r, err := http.NewRequest("GET", "", nil) if err != nil { t.Fatal(err) } err = te.store.ServeBlob(te.ctx, w, r, dr.Digest) if err != nil { t.Fatalf(err.Error()) } dl := digest.FromBytes(w.Body.Bytes()) if dl != dr.Digest { t.Errorf("Mismatching blob fetch from proxy") } } localStats = te.LocalStats() remoteStats = te.RemoteStats() // Ensure remote unchanged sbsMu.Lock() defer sbsMu.Unlock() if (*remoteStats)["stat"] != remoteStatCount && (*remoteStats)["open"] != remoteOpenCount { t.Fatalf("unexpected remote stats: %#v", remoteStats) } } docker-registry-2.6.2~ds1/registry/proxy/proxymanifeststore.go000066400000000000000000000052351313450123100250070ustar00rootroot00000000000000package proxy import ( "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/proxy/scheduler" ) // todo(richardscothern): from cache control header or config const repositoryTTL = time.Duration(24 * 7 * time.Hour) type proxyManifestStore struct { ctx context.Context localManifests distribution.ManifestService remoteManifests distribution.ManifestService repositoryName reference.Named scheduler *scheduler.TTLExpirationScheduler authChallenger authChallenger } var _ distribution.ManifestService = &proxyManifestStore{} func (pms proxyManifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { exists, err := pms.localManifests.Exists(ctx, dgst) if err != nil { return false, err } if exists { return true, nil } if err := pms.authChallenger.tryEstablishChallenges(ctx); err != nil { return false, err } return pms.remoteManifests.Exists(ctx, dgst) } func (pms proxyManifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { // At this point `dgst` was either specified explicitly, or returned by the // tagstore with the most recent association. var fromRemote bool manifest, err := pms.localManifests.Get(ctx, dgst, options...) if err != nil { if err := pms.authChallenger.tryEstablishChallenges(ctx); err != nil { return nil, err } manifest, err = pms.remoteManifests.Get(ctx, dgst, options...) if err != nil { return nil, err } fromRemote = true } _, payload, err := manifest.Payload() if err != nil { return nil, err } proxyMetrics.ManifestPush(uint64(len(payload))) if fromRemote { proxyMetrics.ManifestPull(uint64(len(payload))) _, err = pms.localManifests.Put(ctx, manifest) if err != nil { return nil, err } // Schedule the manifest blob for removal repoBlob, err := reference.WithDigest(pms.repositoryName, dgst) if err != nil { context.GetLogger(ctx).Errorf("Error creating reference: %s", err) return nil, err } pms.scheduler.AddManifest(repoBlob, repositoryTTL) // Ensure the manifest blob is cleaned up //pms.scheduler.AddBlob(blobRef, repositoryTTL) } return manifest, err } func (pms proxyManifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { var d digest.Digest return d, distribution.ErrUnsupported } func (pms proxyManifestStore) Delete(ctx context.Context, dgst digest.Digest) error { return distribution.ErrUnsupported } docker-registry-2.6.2~ds1/registry/proxy/proxymanifeststore_test.go000066400000000000000000000166461313450123100260560ustar00rootroot00000000000000package proxy import ( "io" "sync" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" "github.com/docker/libtrust" ) type statsManifest struct { manifests distribution.ManifestService stats map[string]int } type manifestStoreTestEnv struct { manifestDigest digest.Digest // digest of the signed manifest in the local storage manifests proxyManifestStore } func (te manifestStoreTestEnv) LocalStats() *map[string]int { ls := te.manifests.localManifests.(statsManifest).stats return &ls } func (te manifestStoreTestEnv) RemoteStats() *map[string]int { rs := te.manifests.remoteManifests.(statsManifest).stats return &rs } func (sm statsManifest) Delete(ctx context.Context, dgst digest.Digest) error { sm.stats["delete"]++ return sm.manifests.Delete(ctx, dgst) } func (sm statsManifest) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { sm.stats["exists"]++ return sm.manifests.Exists(ctx, dgst) } func (sm statsManifest) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { sm.stats["get"]++ return sm.manifests.Get(ctx, dgst) } func (sm statsManifest) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { sm.stats["put"]++ return sm.manifests.Put(ctx, manifest) } type mockChallenger struct { sync.Mutex count int } // Called for remote operations only func (m *mockChallenger) tryEstablishChallenges(context.Context) error { m.Lock() defer m.Unlock() m.count++ return nil } func (m *mockChallenger) credentialStore() auth.CredentialStore { return nil } func (m *mockChallenger) challengeManager() challenge.Manager { return nil } func newManifestStoreTestEnv(t *testing.T, name, tag string) *manifestStoreTestEnv { nameRef, err := reference.ParseNamed(name) if err != nil { t.Fatalf("unable to parse reference: %s", err) } k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } ctx := context.Background() truthRegistry, err := storage.NewRegistry(ctx, inmemory.New(), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.Schema1SigningKey(k)) if err != nil { t.Fatalf("error creating registry: %v", err) } truthRepo, err := truthRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } tr, err := truthRepo.Manifests(ctx) if err != nil { t.Fatal(err.Error()) } truthManifests := statsManifest{ manifests: tr, stats: make(map[string]int), } manifestDigest, err := populateRepo(ctx, t, truthRepo, name, tag) if err != nil { t.Fatalf(err.Error()) } localRegistry, err := storage.NewRegistry(ctx, inmemory.New(), storage.BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), storage.EnableRedirect, storage.DisableDigestResumption, storage.Schema1SigningKey(k)) if err != nil { t.Fatalf("error creating registry: %v", err) } localRepo, err := localRegistry.Repository(ctx, nameRef) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } lr, err := localRepo.Manifests(ctx) if err != nil { t.Fatal(err.Error()) } localManifests := statsManifest{ manifests: lr, stats: make(map[string]int), } s := scheduler.New(ctx, inmemory.New(), "/scheduler-state.json") return &manifestStoreTestEnv{ manifestDigest: manifestDigest, manifests: proxyManifestStore{ ctx: ctx, localManifests: localManifests, remoteManifests: truthManifests, scheduler: s, repositoryName: nameRef, authChallenger: &mockChallenger{}, }, } } func populateRepo(ctx context.Context, t *testing.T, repository distribution.Repository, name, tag string) (digest.Digest, error) { m := schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: name, Tag: tag, } for i := 0; i < 2; i++ { wr, err := repository.Blobs(ctx).Create(ctx) if err != nil { t.Fatalf("unexpected error creating test upload: %v", err) } rs, ts, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } dgst := digest.Digest(ts) if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm, err := schema1.Sign(&m, pk) if err != nil { t.Fatalf("error signing manifest: %v", err) } ms, err := repository.Manifests(ctx) if err != nil { t.Fatalf(err.Error()) } dgst, err := ms.Put(ctx, sm) if err != nil { t.Fatalf("unexpected errors putting manifest: %v", err) } return dgst, nil } // TestProxyManifests contains basic acceptance tests // for the pull-through behavior func TestProxyManifests(t *testing.T) { name := "foo/bar" env := newManifestStoreTestEnv(t, name, "latest") localStats := env.LocalStats() remoteStats := env.RemoteStats() ctx := context.Background() // Stat - must check local and remote exists, err := env.manifests.Exists(ctx, env.manifestDigest) if err != nil { t.Fatalf("Error checking existence") } if !exists { t.Errorf("Unexpected non-existant manifest") } if (*localStats)["exists"] != 1 && (*remoteStats)["exists"] != 1 { t.Errorf("Unexpected exists count : \n%v \n%v", localStats, remoteStats) } if env.manifests.authChallenger.(*mockChallenger).count != 1 { t.Fatalf("Expected 1 auth challenge, got %#v", env.manifests.authChallenger) } // Get - should succeed and pull manifest into local _, err = env.manifests.Get(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if (*localStats)["get"] != 1 && (*remoteStats)["get"] != 1 { t.Errorf("Unexpected get count") } if (*localStats)["put"] != 1 { t.Errorf("Expected local put") } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } // Stat - should only go to local exists, err = env.manifests.Exists(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if !exists { t.Errorf("Unexpected non-existant manifest") } if (*localStats)["exists"] != 2 && (*remoteStats)["exists"] != 1 { t.Errorf("Unexpected exists count") } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } // Get proxied - won't require another authchallenge _, err = env.manifests.Get(ctx, env.manifestDigest) if err != nil { t.Fatal(err) } if env.manifests.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenges, got %#v", env.manifests.authChallenger) } } docker-registry-2.6.2~ds1/registry/proxy/proxymetrics.go000066400000000000000000000040031313450123100235620ustar00rootroot00000000000000package proxy import ( "expvar" "sync/atomic" ) // Metrics is used to hold metric counters // related to the proxy type Metrics struct { Requests uint64 Hits uint64 Misses uint64 BytesPulled uint64 BytesPushed uint64 } type proxyMetricsCollector struct { blobMetrics Metrics manifestMetrics Metrics } // BlobPull tracks metrics about blobs pulled into the cache func (pmc *proxyMetricsCollector) BlobPull(bytesPulled uint64) { atomic.AddUint64(&pmc.blobMetrics.Misses, 1) atomic.AddUint64(&pmc.blobMetrics.BytesPulled, bytesPulled) } // BlobPush tracks metrics about blobs pushed to clients func (pmc *proxyMetricsCollector) BlobPush(bytesPushed uint64) { atomic.AddUint64(&pmc.blobMetrics.Requests, 1) atomic.AddUint64(&pmc.blobMetrics.Hits, 1) atomic.AddUint64(&pmc.blobMetrics.BytesPushed, bytesPushed) } // ManifestPull tracks metrics related to Manifests pulled into the cache func (pmc *proxyMetricsCollector) ManifestPull(bytesPulled uint64) { atomic.AddUint64(&pmc.manifestMetrics.Misses, 1) atomic.AddUint64(&pmc.manifestMetrics.BytesPulled, bytesPulled) } // ManifestPush tracks metrics about manifests pushed to clients func (pmc *proxyMetricsCollector) ManifestPush(bytesPushed uint64) { atomic.AddUint64(&pmc.manifestMetrics.Requests, 1) atomic.AddUint64(&pmc.manifestMetrics.Hits, 1) atomic.AddUint64(&pmc.manifestMetrics.BytesPushed, bytesPushed) } // proxyMetrics tracks metrics about the proxy cache. This is // kept globally and made available via expvar. var proxyMetrics = &proxyMetricsCollector{} func init() { registry := expvar.Get("registry") if registry == nil { registry = expvar.NewMap("registry") } pm := registry.(*expvar.Map).Get("proxy") if pm == nil { pm = &expvar.Map{} pm.(*expvar.Map).Init() registry.(*expvar.Map).Set("proxy", pm) } pm.(*expvar.Map).Set("blobs", expvar.Func(func() interface{} { return proxyMetrics.blobMetrics })) pm.(*expvar.Map).Set("manifests", expvar.Func(func() interface{} { return proxyMetrics.manifestMetrics })) } docker-registry-2.6.2~ds1/registry/proxy/proxyregistry.go000066400000000000000000000147671313450123100240060ustar00rootroot00000000000000package proxy import ( "fmt" "net/http" "net/url" "sync" "github.com/docker/distribution" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/client" "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/auth/challenge" "github.com/docker/distribution/registry/client/transport" "github.com/docker/distribution/registry/proxy/scheduler" "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/driver" ) // proxyingRegistry fetches content from a remote registry and caches it locally type proxyingRegistry struct { embedded distribution.Namespace // provides local registry functionality scheduler *scheduler.TTLExpirationScheduler remoteURL url.URL authChallenger authChallenger } // NewRegistryPullThroughCache creates a registry acting as a pull through cache func NewRegistryPullThroughCache(ctx context.Context, registry distribution.Namespace, driver driver.StorageDriver, config configuration.Proxy) (distribution.Namespace, error) { remoteURL, err := url.Parse(config.RemoteURL) if err != nil { return nil, err } v := storage.NewVacuum(ctx, driver) s := scheduler.New(ctx, driver, "/scheduler-state.json") s.OnBlobExpire(func(ref reference.Reference) error { var r reference.Canonical var ok bool if r, ok = ref.(reference.Canonical); !ok { return fmt.Errorf("unexpected reference type : %T", ref) } repo, err := registry.Repository(ctx, r) if err != nil { return err } blobs := repo.Blobs(ctx) // Clear the repository reference and descriptor caches err = blobs.Delete(ctx, r.Digest()) if err != nil { return err } err = v.RemoveBlob(r.Digest().String()) if err != nil { return err } return nil }) s.OnManifestExpire(func(ref reference.Reference) error { var r reference.Canonical var ok bool if r, ok = ref.(reference.Canonical); !ok { return fmt.Errorf("unexpected reference type : %T", ref) } repo, err := registry.Repository(ctx, r) if err != nil { return err } manifests, err := repo.Manifests(ctx) if err != nil { return err } err = manifests.Delete(ctx, r.Digest()) if err != nil { return err } return nil }) err = s.Start() if err != nil { return nil, err } cs, err := configureAuth(config.Username, config.Password, config.RemoteURL) if err != nil { return nil, err } return &proxyingRegistry{ embedded: registry, scheduler: s, remoteURL: *remoteURL, authChallenger: &remoteAuthChallenger{ remoteURL: *remoteURL, cm: challenge.NewSimpleManager(), cs: cs, }, }, nil } func (pr *proxyingRegistry) Scope() distribution.Scope { return distribution.GlobalScope } func (pr *proxyingRegistry) Repositories(ctx context.Context, repos []string, last string) (n int, err error) { return pr.embedded.Repositories(ctx, repos, last) } func (pr *proxyingRegistry) Repository(ctx context.Context, name reference.Named) (distribution.Repository, error) { c := pr.authChallenger tr := transport.NewTransport(http.DefaultTransport, auth.NewAuthorizer(c.challengeManager(), auth.NewTokenHandler(http.DefaultTransport, c.credentialStore(), name.Name(), "pull"))) localRepo, err := pr.embedded.Repository(ctx, name) if err != nil { return nil, err } localManifests, err := localRepo.Manifests(ctx, storage.SkipLayerVerification()) if err != nil { return nil, err } remoteRepo, err := client.NewRepository(ctx, name, pr.remoteURL.String(), tr) if err != nil { return nil, err } remoteManifests, err := remoteRepo.Manifests(ctx) if err != nil { return nil, err } return &proxiedRepository{ blobStore: &proxyBlobStore{ localStore: localRepo.Blobs(ctx), remoteStore: remoteRepo.Blobs(ctx), scheduler: pr.scheduler, repositoryName: name, authChallenger: pr.authChallenger, }, manifests: &proxyManifestStore{ repositoryName: name, localManifests: localManifests, // Options? remoteManifests: remoteManifests, ctx: ctx, scheduler: pr.scheduler, authChallenger: pr.authChallenger, }, name: name, tags: &proxyTagService{ localTags: localRepo.Tags(ctx), remoteTags: remoteRepo.Tags(ctx), authChallenger: pr.authChallenger, }, }, nil } func (pr *proxyingRegistry) Blobs() distribution.BlobEnumerator { return pr.embedded.Blobs() } func (pr *proxyingRegistry) BlobStatter() distribution.BlobStatter { return pr.embedded.BlobStatter() } // authChallenger encapsulates a request to the upstream to establish credential challenges type authChallenger interface { tryEstablishChallenges(context.Context) error challengeManager() challenge.Manager credentialStore() auth.CredentialStore } type remoteAuthChallenger struct { remoteURL url.URL sync.Mutex cm challenge.Manager cs auth.CredentialStore } func (r *remoteAuthChallenger) credentialStore() auth.CredentialStore { return r.cs } func (r *remoteAuthChallenger) challengeManager() challenge.Manager { return r.cm } // tryEstablishChallenges will attempt to get a challenge type for the upstream if none currently exist func (r *remoteAuthChallenger) tryEstablishChallenges(ctx context.Context) error { r.Lock() defer r.Unlock() remoteURL := r.remoteURL remoteURL.Path = "/v2/" challenges, err := r.cm.GetChallenges(remoteURL) if err != nil { return err } if len(challenges) > 0 { return nil } // establish challenge type with upstream if err := ping(r.cm, remoteURL.String(), challengeHeader); err != nil { return err } context.GetLogger(ctx).Infof("Challenge established with upstream : %s %s", remoteURL, r.cm) return nil } // proxiedRepository uses proxying blob and manifest services to serve content // locally, or pulling it through from a remote and caching it locally if it doesn't // already exist type proxiedRepository struct { blobStore distribution.BlobStore manifests distribution.ManifestService name reference.Named tags distribution.TagService } func (pr *proxiedRepository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { return pr.manifests, nil } func (pr *proxiedRepository) Blobs(ctx context.Context) distribution.BlobStore { return pr.blobStore } func (pr *proxiedRepository) Named() reference.Named { return pr.name } func (pr *proxiedRepository) Tags(ctx context.Context) distribution.TagService { return pr.tags } docker-registry-2.6.2~ds1/registry/proxy/proxytagservice.go000066400000000000000000000033361313450123100242600ustar00rootroot00000000000000package proxy import ( "github.com/docker/distribution" "github.com/docker/distribution/context" ) // proxyTagService supports local and remote lookup of tags. type proxyTagService struct { localTags distribution.TagService remoteTags distribution.TagService authChallenger authChallenger } var _ distribution.TagService = proxyTagService{} // Get attempts to get the most recent digest for the tag by checking the remote // tag service first and then caching it locally. If the remote is unavailable // the local association is returned func (pt proxyTagService) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { err := pt.authChallenger.tryEstablishChallenges(ctx) if err == nil { desc, err := pt.remoteTags.Get(ctx, tag) if err == nil { err := pt.localTags.Tag(ctx, tag, desc) if err != nil { return distribution.Descriptor{}, err } return desc, nil } } desc, err := pt.localTags.Get(ctx, tag) if err != nil { return distribution.Descriptor{}, err } return desc, nil } func (pt proxyTagService) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { return distribution.ErrUnsupported } func (pt proxyTagService) Untag(ctx context.Context, tag string) error { err := pt.localTags.Untag(ctx, tag) if err != nil { return err } return nil } func (pt proxyTagService) All(ctx context.Context) ([]string, error) { err := pt.authChallenger.tryEstablishChallenges(ctx) if err == nil { tags, err := pt.remoteTags.All(ctx) if err == nil { return tags, err } } return pt.localTags.All(ctx) } func (pt proxyTagService) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { return []string{}, distribution.ErrUnsupported } docker-registry-2.6.2~ds1/registry/proxy/proxytagservice_test.go000066400000000000000000000102771313450123100253210ustar00rootroot00000000000000package proxy import ( "reflect" "sort" "sync" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" ) type mockTagStore struct { mapping map[string]distribution.Descriptor sync.Mutex } var _ distribution.TagService = &mockTagStore{} func (m *mockTagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { m.Lock() defer m.Unlock() if d, ok := m.mapping[tag]; ok { return d, nil } return distribution.Descriptor{}, distribution.ErrTagUnknown{} } func (m *mockTagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { m.Lock() defer m.Unlock() m.mapping[tag] = desc return nil } func (m *mockTagStore) Untag(ctx context.Context, tag string) error { m.Lock() defer m.Unlock() if _, ok := m.mapping[tag]; ok { delete(m.mapping, tag) return nil } return distribution.ErrTagUnknown{} } func (m *mockTagStore) All(ctx context.Context) ([]string, error) { m.Lock() defer m.Unlock() var tags []string for tag := range m.mapping { tags = append(tags, tag) } return tags, nil } func (m *mockTagStore) Lookup(ctx context.Context, digest distribution.Descriptor) ([]string, error) { panic("not implemented") } func testProxyTagService(local, remote map[string]distribution.Descriptor) *proxyTagService { if local == nil { local = make(map[string]distribution.Descriptor) } if remote == nil { remote = make(map[string]distribution.Descriptor) } return &proxyTagService{ localTags: &mockTagStore{mapping: local}, remoteTags: &mockTagStore{mapping: remote}, authChallenger: &mockChallenger{}, } } func TestGet(t *testing.T) { remoteDesc := distribution.Descriptor{Size: 42} remoteTag := "remote" proxyTags := testProxyTagService(map[string]distribution.Descriptor{remoteTag: remoteDesc}, nil) ctx := context.Background() // Get pre-loaded tag d, err := proxyTags.Get(ctx, remoteTag) if err != nil { t.Fatal(err) } if proxyTags.authChallenger.(*mockChallenger).count != 1 { t.Fatalf("Expected 1 auth challenge call, got %#v", proxyTags.authChallenger) } if !reflect.DeepEqual(d, remoteDesc) { t.Fatal("unable to get put tag") } local, err := proxyTags.localTags.Get(ctx, remoteTag) if err != nil { t.Fatal("remote tag not pulled into store") } if !reflect.DeepEqual(local, remoteDesc) { t.Fatalf("unexpected descriptor pulled through") } // Manually overwrite remote tag newRemoteDesc := distribution.Descriptor{Size: 43} err = proxyTags.remoteTags.Tag(ctx, remoteTag, newRemoteDesc) if err != nil { t.Fatal(err) } d, err = proxyTags.Get(ctx, remoteTag) if err != nil { t.Fatal(err) } if proxyTags.authChallenger.(*mockChallenger).count != 2 { t.Fatalf("Expected 2 auth challenge calls, got %#v", proxyTags.authChallenger) } if !reflect.DeepEqual(d, newRemoteDesc) { t.Fatal("unable to get put tag") } _, err = proxyTags.localTags.Get(ctx, remoteTag) if err != nil { t.Fatal("remote tag not pulled into store") } // untag, ensure it's removed locally, but present in remote err = proxyTags.Untag(ctx, remoteTag) if err != nil { t.Fatal(err) } _, err = proxyTags.localTags.Get(ctx, remoteTag) if err == nil { t.Fatalf("Expected error getting Untag'd tag") } _, err = proxyTags.remoteTags.Get(ctx, remoteTag) if err != nil { t.Fatalf("remote tag should not be untagged with proxyTag.Untag") } _, err = proxyTags.Get(ctx, remoteTag) if err != nil { t.Fatal("untagged tag should be pulled through") } if proxyTags.authChallenger.(*mockChallenger).count != 3 { t.Fatalf("Expected 3 auth challenge calls, got %#v", proxyTags.authChallenger) } // Add another tag. Ensure both tags appear in 'All' err = proxyTags.remoteTags.Tag(ctx, "funtag", distribution.Descriptor{Size: 42}) if err != nil { t.Fatal(err) } all, err := proxyTags.All(ctx) if err != nil { t.Fatal(err) } if len(all) != 2 { t.Fatalf("Unexpected tag length returned from All() : %d ", len(all)) } sort.Strings(all) if all[0] != "funtag" && all[1] != "remote" { t.Fatalf("Unexpected tags returned from All() : %v ", all) } if proxyTags.authChallenger.(*mockChallenger).count != 4 { t.Fatalf("Expected 4 auth challenge calls, got %#v", proxyTags.authChallenger) } } docker-registry-2.6.2~ds1/registry/proxy/scheduler/000077500000000000000000000000001313450123100224445ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/proxy/scheduler/scheduler.go000066400000000000000000000136041313450123100247550ustar00rootroot00000000000000package scheduler import ( "encoding/json" "fmt" "sync" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver" ) // onTTLExpiryFunc is called when a repository's TTL expires type expiryFunc func(reference.Reference) error const ( entryTypeBlob = iota entryTypeManifest indexSaveFrequency = 5 * time.Second ) // schedulerEntry represents an entry in the scheduler // fields are exported for serialization type schedulerEntry struct { Key string `json:"Key"` Expiry time.Time `json:"ExpiryData"` EntryType int `json:"EntryType"` timer *time.Timer } // New returns a new instance of the scheduler func New(ctx context.Context, driver driver.StorageDriver, path string) *TTLExpirationScheduler { return &TTLExpirationScheduler{ entries: make(map[string]*schedulerEntry), driver: driver, pathToStateFile: path, ctx: ctx, stopped: true, doneChan: make(chan struct{}), saveTimer: time.NewTicker(indexSaveFrequency), } } // TTLExpirationScheduler is a scheduler used to perform actions // when TTLs expire type TTLExpirationScheduler struct { sync.Mutex entries map[string]*schedulerEntry driver driver.StorageDriver ctx context.Context pathToStateFile string stopped bool onBlobExpire expiryFunc onManifestExpire expiryFunc indexDirty bool saveTimer *time.Ticker doneChan chan struct{} } // OnBlobExpire is called when a scheduled blob's TTL expires func (ttles *TTLExpirationScheduler) OnBlobExpire(f expiryFunc) { ttles.Lock() defer ttles.Unlock() ttles.onBlobExpire = f } // OnManifestExpire is called when a scheduled manifest's TTL expires func (ttles *TTLExpirationScheduler) OnManifestExpire(f expiryFunc) { ttles.Lock() defer ttles.Unlock() ttles.onManifestExpire = f } // AddBlob schedules a blob cleanup after ttl expires func (ttles *TTLExpirationScheduler) AddBlob(blobRef reference.Canonical, ttl time.Duration) error { ttles.Lock() defer ttles.Unlock() if ttles.stopped { return fmt.Errorf("scheduler not started") } ttles.add(blobRef, ttl, entryTypeBlob) return nil } // AddManifest schedules a manifest cleanup after ttl expires func (ttles *TTLExpirationScheduler) AddManifest(manifestRef reference.Canonical, ttl time.Duration) error { ttles.Lock() defer ttles.Unlock() if ttles.stopped { return fmt.Errorf("scheduler not started") } ttles.add(manifestRef, ttl, entryTypeManifest) return nil } // Start starts the scheduler func (ttles *TTLExpirationScheduler) Start() error { ttles.Lock() defer ttles.Unlock() err := ttles.readState() if err != nil { return err } if !ttles.stopped { return fmt.Errorf("Scheduler already started") } context.GetLogger(ttles.ctx).Infof("Starting cached object TTL expiration scheduler...") ttles.stopped = false // Start timer for each deserialized entry for _, entry := range ttles.entries { entry.timer = ttles.startTimer(entry, entry.Expiry.Sub(time.Now())) } // Start a ticker to periodically save the entries index go func() { for { select { case <-ttles.saveTimer.C: ttles.Lock() if !ttles.indexDirty { ttles.Unlock() continue } err := ttles.writeState() if err != nil { context.GetLogger(ttles.ctx).Errorf("Error writing scheduler state: %s", err) } else { ttles.indexDirty = false } ttles.Unlock() case <-ttles.doneChan: return } } }() return nil } func (ttles *TTLExpirationScheduler) add(r reference.Reference, ttl time.Duration, eType int) { entry := &schedulerEntry{ Key: r.String(), Expiry: time.Now().Add(ttl), EntryType: eType, } context.GetLogger(ttles.ctx).Infof("Adding new scheduler entry for %s with ttl=%s", entry.Key, entry.Expiry.Sub(time.Now())) if oldEntry, present := ttles.entries[entry.Key]; present && oldEntry.timer != nil { oldEntry.timer.Stop() } ttles.entries[entry.Key] = entry entry.timer = ttles.startTimer(entry, ttl) ttles.indexDirty = true } func (ttles *TTLExpirationScheduler) startTimer(entry *schedulerEntry, ttl time.Duration) *time.Timer { return time.AfterFunc(ttl, func() { ttles.Lock() defer ttles.Unlock() var f expiryFunc switch entry.EntryType { case entryTypeBlob: f = ttles.onBlobExpire case entryTypeManifest: f = ttles.onManifestExpire default: f = func(reference.Reference) error { return fmt.Errorf("scheduler entry type") } } ref, err := reference.Parse(entry.Key) if err == nil { if err := f(ref); err != nil { context.GetLogger(ttles.ctx).Errorf("Scheduler error returned from OnExpire(%s): %s", entry.Key, err) } } else { context.GetLogger(ttles.ctx).Errorf("Error unpacking reference: %s", err) } delete(ttles.entries, entry.Key) ttles.indexDirty = true }) } // Stop stops the scheduler. func (ttles *TTLExpirationScheduler) Stop() { ttles.Lock() defer ttles.Unlock() if err := ttles.writeState(); err != nil { context.GetLogger(ttles.ctx).Errorf("Error writing scheduler state: %s", err) } for _, entry := range ttles.entries { entry.timer.Stop() } close(ttles.doneChan) ttles.saveTimer.Stop() ttles.stopped = true } func (ttles *TTLExpirationScheduler) writeState() error { jsonBytes, err := json.Marshal(ttles.entries) if err != nil { return err } err = ttles.driver.PutContent(ttles.ctx, ttles.pathToStateFile, jsonBytes) if err != nil { return err } return nil } func (ttles *TTLExpirationScheduler) readState() error { if _, err := ttles.driver.Stat(ttles.ctx, ttles.pathToStateFile); err != nil { switch err := err.(type) { case driver.PathNotFoundError: return nil default: return err } } bytes, err := ttles.driver.GetContent(ttles.ctx, ttles.pathToStateFile) if err != nil { return err } err = json.Unmarshal(bytes, &ttles.entries) if err != nil { return err } return nil } docker-registry-2.6.2~ds1/registry/proxy/scheduler/scheduler_test.go000066400000000000000000000115461313450123100260170ustar00rootroot00000000000000package scheduler import ( "encoding/json" "sync" "testing" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver/inmemory" ) func testRefs(t *testing.T) (reference.Reference, reference.Reference, reference.Reference) { ref1, err := reference.Parse("testrepo@sha256:aaaaeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") if err != nil { t.Fatalf("could not parse reference: %v", err) } ref2, err := reference.Parse("testrepo@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb") if err != nil { t.Fatalf("could not parse reference: %v", err) } ref3, err := reference.Parse("testrepo@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc") if err != nil { t.Fatalf("could not parse reference: %v", err) } return ref1, ref2, ref3 } func TestSchedule(t *testing.T) { ref1, ref2, ref3 := testRefs(t) timeUnit := time.Millisecond remainingRepos := map[string]bool{ ref1.String(): true, ref2.String(): true, ref3.String(): true, } var mu sync.Mutex s := New(context.Background(), inmemory.New(), "/ttl") deleteFunc := func(repoName reference.Reference) error { if len(remainingRepos) == 0 { t.Fatalf("Incorrect expiry count") } _, ok := remainingRepos[repoName.String()] if !ok { t.Fatalf("Trying to remove nonexistent repo: %s", repoName) } t.Log("removing", repoName) mu.Lock() delete(remainingRepos, repoName.String()) mu.Unlock() return nil } s.onBlobExpire = deleteFunc err := s.Start() if err != nil { t.Fatalf("Error starting ttlExpirationScheduler: %s", err) } s.add(ref1, 3*timeUnit, entryTypeBlob) s.add(ref2, 1*timeUnit, entryTypeBlob) func() { s.Lock() s.add(ref3, 1*timeUnit, entryTypeBlob) s.Unlock() }() // Ensure all repos are deleted <-time.After(50 * timeUnit) mu.Lock() defer mu.Unlock() if len(remainingRepos) != 0 { t.Fatalf("Repositories remaining: %#v", remainingRepos) } } func TestRestoreOld(t *testing.T) { ref1, ref2, _ := testRefs(t) remainingRepos := map[string]bool{ ref1.String(): true, ref2.String(): true, } var wg sync.WaitGroup wg.Add(len(remainingRepos)) var mu sync.Mutex deleteFunc := func(r reference.Reference) error { mu.Lock() defer mu.Unlock() if r.String() == ref1.String() && len(remainingRepos) == 2 { t.Errorf("ref1 should not be removed first") } _, ok := remainingRepos[r.String()] if !ok { t.Fatalf("Trying to remove nonexistent repo: %s", r) } delete(remainingRepos, r.String()) wg.Done() return nil } timeUnit := time.Millisecond serialized, err := json.Marshal(&map[string]schedulerEntry{ ref1.String(): { Expiry: time.Now().Add(10 * timeUnit), Key: ref1.String(), EntryType: 0, }, ref2.String(): { Expiry: time.Now().Add(-3 * timeUnit), // TTL passed, should be removed first Key: ref2.String(), EntryType: 0, }, }) if err != nil { t.Fatalf("Error serializing test data: %s", err.Error()) } ctx := context.Background() pathToStatFile := "/ttl" fs := inmemory.New() err = fs.PutContent(ctx, pathToStatFile, serialized) if err != nil { t.Fatal("Unable to write serialized data to fs") } s := New(context.Background(), fs, "/ttl") s.OnBlobExpire(deleteFunc) err = s.Start() if err != nil { t.Fatalf("Error starting ttlExpirationScheduler: %s", err) } defer s.Stop() wg.Wait() mu.Lock() defer mu.Unlock() if len(remainingRepos) != 0 { t.Fatalf("Repositories remaining: %#v", remainingRepos) } } func TestStopRestore(t *testing.T) { ref1, ref2, _ := testRefs(t) timeUnit := time.Millisecond remainingRepos := map[string]bool{ ref1.String(): true, ref2.String(): true, } var mu sync.Mutex deleteFunc := func(r reference.Reference) error { mu.Lock() delete(remainingRepos, r.String()) mu.Unlock() return nil } fs := inmemory.New() pathToStateFile := "/ttl" s := New(context.Background(), fs, pathToStateFile) s.onBlobExpire = deleteFunc err := s.Start() if err != nil { t.Fatalf(err.Error()) } s.add(ref1, 300*timeUnit, entryTypeBlob) s.add(ref2, 100*timeUnit, entryTypeBlob) // Start and stop before all operations complete // state will be written to fs s.Stop() time.Sleep(10 * time.Millisecond) // v2 will restore state from fs s2 := New(context.Background(), fs, pathToStateFile) s2.onBlobExpire = deleteFunc err = s2.Start() if err != nil { t.Fatalf("Error starting v2: %s", err.Error()) } <-time.After(500 * timeUnit) mu.Lock() defer mu.Unlock() if len(remainingRepos) != 0 { t.Fatalf("Repositories remaining: %#v", remainingRepos) } } func TestDoubleStart(t *testing.T) { s := New(context.Background(), inmemory.New(), "/ttl") err := s.Start() if err != nil { t.Fatalf("Unable to start scheduler") } err = s.Start() if err == nil { t.Fatalf("Scheduler started twice without error") } } docker-registry-2.6.2~ds1/registry/registry.go000066400000000000000000000233731313450123100215140ustar00rootroot00000000000000package registry import ( "crypto/tls" "crypto/x509" "fmt" "io/ioutil" "net/http" "os" "time" "rsc.io/letsencrypt" log "github.com/Sirupsen/logrus" "github.com/Sirupsen/logrus/formatters/logstash" "github.com/bugsnag/bugsnag-go" "github.com/docker/distribution/configuration" "github.com/docker/distribution/context" "github.com/docker/distribution/health" "github.com/docker/distribution/registry/handlers" "github.com/docker/distribution/registry/listener" "github.com/docker/distribution/uuid" "github.com/docker/distribution/version" gorhandlers "github.com/gorilla/handlers" "github.com/spf13/cobra" "github.com/yvasiyarov/gorelic" ) // ServeCmd is a cobra command for running the registry. var ServeCmd = &cobra.Command{ Use: "serve ", Short: "`serve` stores and distributes Docker images", Long: "`serve` stores and distributes Docker images.", Run: func(cmd *cobra.Command, args []string) { // setup context ctx := context.WithVersion(context.Background(), version.Version) config, err := resolveConfiguration(args) if err != nil { fmt.Fprintf(os.Stderr, "configuration error: %v\n", err) cmd.Usage() os.Exit(1) } if config.HTTP.Debug.Addr != "" { go func(addr string) { log.Infof("debug server listening %v", addr) if err := http.ListenAndServe(addr, nil); err != nil { log.Fatalf("error listening on debug interface: %v", err) } }(config.HTTP.Debug.Addr) } registry, err := NewRegistry(ctx, config) if err != nil { log.Fatalln(err) } if err = registry.ListenAndServe(); err != nil { log.Fatalln(err) } }, } // A Registry represents a complete instance of the registry. // TODO(aaronl): It might make sense for Registry to become an interface. type Registry struct { config *configuration.Configuration app *handlers.App server *http.Server } // NewRegistry creates a new registry from a context and configuration struct. func NewRegistry(ctx context.Context, config *configuration.Configuration) (*Registry, error) { var err error ctx, err = configureLogging(ctx, config) if err != nil { return nil, fmt.Errorf("error configuring logger: %v", err) } // inject a logger into the uuid library. warns us if there is a problem // with uuid generation under low entropy. uuid.Loggerf = context.GetLogger(ctx).Warnf app := handlers.NewApp(ctx, config) // TODO(aaronl): The global scope of the health checks means NewRegistry // can only be called once per process. app.RegisterHealthChecks() handler := configureReporting(app) handler = alive("/", handler) handler = health.Handler(handler) handler = panicHandler(handler) if !config.Log.AccessLog.Disabled { handler = gorhandlers.CombinedLoggingHandler(os.Stdout, handler) } server := &http.Server{ Handler: handler, } return &Registry{ app: app, config: config, server: server, }, nil } // ListenAndServe runs the registry's HTTP server. func (registry *Registry) ListenAndServe() error { config := registry.config ln, err := listener.NewListener(config.HTTP.Net, config.HTTP.Addr) if err != nil { return err } if config.HTTP.TLS.Certificate != "" || config.HTTP.TLS.LetsEncrypt.CacheFile != "" { tlsConf := &tls.Config{ ClientAuth: tls.NoClientCert, NextProtos: nextProtos(config), MinVersion: tls.VersionTLS10, PreferServerCipherSuites: true, CipherSuites: []uint16{ tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, }, } if config.HTTP.TLS.LetsEncrypt.CacheFile != "" { if config.HTTP.TLS.Certificate != "" { return fmt.Errorf("cannot specify both certificate and Let's Encrypt") } var m letsencrypt.Manager if err := m.CacheFile(config.HTTP.TLS.LetsEncrypt.CacheFile); err != nil { return err } if !m.Registered() { if err := m.Register(config.HTTP.TLS.LetsEncrypt.Email, nil); err != nil { return err } } tlsConf.GetCertificate = m.GetCertificate } else { tlsConf.Certificates = make([]tls.Certificate, 1) tlsConf.Certificates[0], err = tls.LoadX509KeyPair(config.HTTP.TLS.Certificate, config.HTTP.TLS.Key) if err != nil { return err } } if len(config.HTTP.TLS.ClientCAs) != 0 { pool := x509.NewCertPool() for _, ca := range config.HTTP.TLS.ClientCAs { caPem, err := ioutil.ReadFile(ca) if err != nil { return err } if ok := pool.AppendCertsFromPEM(caPem); !ok { return fmt.Errorf("Could not add CA to pool") } } for _, subj := range pool.Subjects() { context.GetLogger(registry.app).Debugf("CA Subject: %s", string(subj)) } tlsConf.ClientAuth = tls.RequireAndVerifyClientCert tlsConf.ClientCAs = pool } ln = tls.NewListener(ln, tlsConf) context.GetLogger(registry.app).Infof("listening on %v, tls", ln.Addr()) } else { context.GetLogger(registry.app).Infof("listening on %v", ln.Addr()) } return registry.server.Serve(ln) } func configureReporting(app *handlers.App) http.Handler { var handler http.Handler = app if app.Config.Reporting.Bugsnag.APIKey != "" { bugsnagConfig := bugsnag.Configuration{ APIKey: app.Config.Reporting.Bugsnag.APIKey, // TODO(brianbland): provide the registry version here // AppVersion: "2.0", } if app.Config.Reporting.Bugsnag.ReleaseStage != "" { bugsnagConfig.ReleaseStage = app.Config.Reporting.Bugsnag.ReleaseStage } if app.Config.Reporting.Bugsnag.Endpoint != "" { bugsnagConfig.Endpoint = app.Config.Reporting.Bugsnag.Endpoint } bugsnag.Configure(bugsnagConfig) handler = bugsnag.Handler(handler) } if app.Config.Reporting.NewRelic.LicenseKey != "" { agent := gorelic.NewAgent() agent.NewrelicLicense = app.Config.Reporting.NewRelic.LicenseKey if app.Config.Reporting.NewRelic.Name != "" { agent.NewrelicName = app.Config.Reporting.NewRelic.Name } agent.CollectHTTPStat = true agent.Verbose = app.Config.Reporting.NewRelic.Verbose agent.Run() handler = agent.WrapHTTPHandler(handler) } return handler } // configureLogging prepares the context with a logger using the // configuration. func configureLogging(ctx context.Context, config *configuration.Configuration) (context.Context, error) { if config.Log.Level == "" && config.Log.Formatter == "" { // If no config for logging is set, fallback to deprecated "Loglevel". log.SetLevel(logLevel(config.Loglevel)) ctx = context.WithLogger(ctx, context.GetLogger(ctx)) return ctx, nil } log.SetLevel(logLevel(config.Log.Level)) formatter := config.Log.Formatter if formatter == "" { formatter = "text" // default formatter } switch formatter { case "json": log.SetFormatter(&log.JSONFormatter{ TimestampFormat: time.RFC3339Nano, }) case "text": log.SetFormatter(&log.TextFormatter{ TimestampFormat: time.RFC3339Nano, }) case "logstash": log.SetFormatter(&logstash.LogstashFormatter{ TimestampFormat: time.RFC3339Nano, }) default: // just let the library use default on empty string. if config.Log.Formatter != "" { return ctx, fmt.Errorf("unsupported logging formatter: %q", config.Log.Formatter) } } if config.Log.Formatter != "" { log.Debugf("using %q logging formatter", config.Log.Formatter) } if len(config.Log.Fields) > 0 { // build up the static fields, if present. var fields []interface{} for k := range config.Log.Fields { fields = append(fields, k) } ctx = context.WithValues(ctx, config.Log.Fields) ctx = context.WithLogger(ctx, context.GetLogger(ctx, fields...)) } return ctx, nil } func logLevel(level configuration.Loglevel) log.Level { l, err := log.ParseLevel(string(level)) if err != nil { l = log.InfoLevel log.Warnf("error parsing level %q: %v, using %q ", level, err, l) } return l } // panicHandler add an HTTP handler to web app. The handler recover the happening // panic. logrus.Panic transmits panic message to pre-config log hooks, which is // defined in config.yml. func panicHandler(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if err := recover(); err != nil { log.Panic(fmt.Sprintf("%v", err)) } }() handler.ServeHTTP(w, r) }) } // alive simply wraps the handler with a route that always returns an http 200 // response when the path is matched. If the path is not matched, the request // is passed to the provided handler. There is no guarantee of anything but // that the server is up. Wrap with other handlers (such as health.Handler) // for greater affect. func alive(path string, handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == path { w.Header().Set("Cache-Control", "no-cache") w.WriteHeader(http.StatusOK) return } handler.ServeHTTP(w, r) }) } func resolveConfiguration(args []string) (*configuration.Configuration, error) { var configurationPath string if len(args) > 0 { configurationPath = args[0] } else if os.Getenv("REGISTRY_CONFIGURATION_PATH") != "" { configurationPath = os.Getenv("REGISTRY_CONFIGURATION_PATH") } if configurationPath == "" { return nil, fmt.Errorf("configuration path unspecified") } fp, err := os.Open(configurationPath) if err != nil { return nil, err } defer fp.Close() config, err := configuration.Parse(fp) if err != nil { return nil, fmt.Errorf("error parsing %s: %v", configurationPath, err) } return config, nil } func nextProtos(config *configuration.Configuration) []string { switch config.HTTP.HTTP2.Disabled { case true: return []string{"http/1.1"} default: return []string{"h2", "http/1.1"} } } docker-registry-2.6.2~ds1/registry/registry_test.go000066400000000000000000000017611313450123100225500ustar00rootroot00000000000000package registry import ( "reflect" "testing" "github.com/docker/distribution/configuration" ) // Tests to ensure nextProtos returns the correct protocols when: // * config.HTTP.HTTP2.Disabled is not explicitly set => [h2 http/1.1] // * config.HTTP.HTTP2.Disabled is explicitly set to false [h2 http/1.1] // * config.HTTP.HTTP2.Disabled is explicitly set to true [http/1.1] func TestNextProtos(t *testing.T) { config := &configuration.Configuration{} protos := nextProtos(config) if !reflect.DeepEqual(protos, []string{"h2", "http/1.1"}) { t.Fatalf("expected protos to equal [h2 http/1.1], got %s", protos) } config.HTTP.HTTP2.Disabled = false protos = nextProtos(config) if !reflect.DeepEqual(protos, []string{"h2", "http/1.1"}) { t.Fatalf("expected protos to equal [h2 http/1.1], got %s", protos) } config.HTTP.HTTP2.Disabled = true protos = nextProtos(config) if !reflect.DeepEqual(protos, []string{"http/1.1"}) { t.Fatalf("expected protos to equal [http/1.1], got %s", protos) } } docker-registry-2.6.2~ds1/registry/root.go000066400000000000000000000043131313450123100206200ustar00rootroot00000000000000package registry import ( "fmt" "os" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage" "github.com/docker/distribution/registry/storage/driver/factory" "github.com/docker/distribution/version" "github.com/docker/libtrust" "github.com/spf13/cobra" ) var showVersion bool func init() { RootCmd.AddCommand(ServeCmd) RootCmd.AddCommand(GCCmd) GCCmd.Flags().BoolVarP(&dryRun, "dry-run", "d", false, "do everything except remove the blobs") RootCmd.Flags().BoolVarP(&showVersion, "version", "v", false, "show the version and exit") } // RootCmd is the main command for the 'registry' binary. var RootCmd = &cobra.Command{ Use: "registry", Short: "`registry`", Long: "`registry`", Run: func(cmd *cobra.Command, args []string) { if showVersion { version.PrintVersion() return } cmd.Usage() }, } var dryRun bool // GCCmd is the cobra command that corresponds to the garbage-collect subcommand var GCCmd = &cobra.Command{ Use: "garbage-collect ", Short: "`garbage-collect` deletes layers not referenced by any manifests", Long: "`garbage-collect` deletes layers not referenced by any manifests", Run: func(cmd *cobra.Command, args []string) { config, err := resolveConfiguration(args) if err != nil { fmt.Fprintf(os.Stderr, "configuration error: %v\n", err) cmd.Usage() os.Exit(1) } driver, err := factory.Create(config.Storage.Type(), config.Storage.Parameters()) if err != nil { fmt.Fprintf(os.Stderr, "failed to construct %s driver: %v", config.Storage.Type(), err) os.Exit(1) } ctx := context.Background() ctx, err = configureLogging(ctx, config) if err != nil { fmt.Fprintf(os.Stderr, "unable to configure logging with config: %s", err) os.Exit(1) } k, err := libtrust.GenerateECP256PrivateKey() if err != nil { fmt.Fprint(os.Stderr, err) os.Exit(1) } registry, err := storage.NewRegistry(ctx, driver, storage.Schema1SigningKey(k)) if err != nil { fmt.Fprintf(os.Stderr, "failed to construct registry: %v", err) os.Exit(1) } err = storage.MarkAndSweep(ctx, driver, registry, dryRun) if err != nil { fmt.Fprintf(os.Stderr, "failed to garbage collect: %v", err) os.Exit(1) } }, } docker-registry-2.6.2~ds1/registry/storage/000077500000000000000000000000001313450123100207515ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/blob_test.go000066400000000000000000000415361313450123100232660ustar00rootroot00000000000000package storage import ( "bytes" "crypto/sha256" "fmt" "io" "io/ioutil" "os" "path" "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver/testdriver" "github.com/docker/distribution/testutil" ) // TestWriteSeek tests that the current file size can be // obtained using Seek func TestWriteSeek(t *testing.T) { ctx := context.Background() imageName, _ := reference.ParseNamed("foo/bar") driver := testdriver.New() registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err := registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } bs := repository.Blobs(ctx) blobUpload, err := bs.Create(ctx) if err != nil { t.Fatalf("unexpected error starting layer upload: %s", err) } contents := []byte{1, 2, 3} blobUpload.Write(contents) blobUpload.Close() offset := blobUpload.Size() if offset != int64(len(contents)) { t.Fatalf("unexpected value for blobUpload offset: %v != %v", offset, len(contents)) } } // TestSimpleBlobUpload covers the blob upload process, exercising common // error paths that might be seen during an upload. func TestSimpleBlobUpload(t *testing.T) { randomDataReader, dgst, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random reader: %v", err) } ctx := context.Background() imageName, _ := reference.ParseNamed("foo/bar") driver := testdriver.New() registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err := registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } bs := repository.Blobs(ctx) h := sha256.New() rd := io.TeeReader(randomDataReader, h) blobUpload, err := bs.Create(ctx) if err != nil { t.Fatalf("unexpected error starting layer upload: %s", err) } // Cancel the upload then restart it if err := blobUpload.Cancel(ctx); err != nil { t.Fatalf("unexpected error during upload cancellation: %v", err) } // get the enclosing directory uploadPath := path.Dir(blobUpload.(*blobWriter).path) // ensure state was cleaned up _, err = driver.List(ctx, uploadPath) if err == nil { t.Fatal("files in upload path after cleanup") } // Do a resume, get unknown upload blobUpload, err = bs.Resume(ctx, blobUpload.ID()) if err != distribution.ErrBlobUploadUnknown { t.Fatalf("unexpected error resuming upload, should be unknown: %v", err) } // Restart! blobUpload, err = bs.Create(ctx) if err != nil { t.Fatalf("unexpected error starting layer upload: %s", err) } // Get the size of our random tarfile randomDataSize, err := seekerSize(randomDataReader) if err != nil { t.Fatalf("error getting seeker size of random data: %v", err) } nn, err := io.Copy(blobUpload, rd) if err != nil { t.Fatalf("unexpected error uploading layer data: %v", err) } if nn != randomDataSize { t.Fatalf("layer data write incomplete") } blobUpload.Close() offset := blobUpload.Size() if offset != nn { t.Fatalf("blobUpload not updated with correct offset: %v != %v", offset, nn) } // Do a resume, for good fun blobUpload, err = bs.Resume(ctx, blobUpload.ID()) if err != nil { t.Fatalf("unexpected error resuming upload: %v", err) } sha256Digest := digest.NewDigest("sha256", h) desc, err := blobUpload.Commit(ctx, distribution.Descriptor{Digest: dgst}) if err != nil { t.Fatalf("unexpected error finishing layer upload: %v", err) } // ensure state was cleaned up uploadPath = path.Dir(blobUpload.(*blobWriter).path) _, err = driver.List(ctx, uploadPath) if err == nil { t.Fatal("files in upload path after commit") } // After finishing an upload, it should no longer exist. if _, err := bs.Resume(ctx, blobUpload.ID()); err != distribution.ErrBlobUploadUnknown { t.Fatalf("expected layer upload to be unknown, got %v", err) } // Test for existence. statDesc, err := bs.Stat(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error checking for existence: %v, %#v", err, bs) } if !reflect.DeepEqual(statDesc, desc) { t.Fatalf("descriptors not equal: %v != %v", statDesc, desc) } rc, err := bs.Open(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error opening blob for read: %v", err) } defer rc.Close() h.Reset() nn, err = io.Copy(h, rc) if err != nil { t.Fatalf("error reading layer: %v", err) } if nn != randomDataSize { t.Fatalf("incorrect read length") } if digest.NewDigest("sha256", h) != sha256Digest { t.Fatalf("unexpected digest from uploaded layer: %q != %q", digest.NewDigest("sha256", h), sha256Digest) } // Delete a blob err = bs.Delete(ctx, desc.Digest) if err != nil { t.Fatalf("Unexpected error deleting blob") } d, err := bs.Stat(ctx, desc.Digest) if err == nil { t.Fatalf("unexpected non-error stating deleted blob: %v", d) } switch err { case distribution.ErrBlobUnknown: break default: t.Errorf("Unexpected error type stat-ing deleted manifest: %#v", err) } _, err = bs.Open(ctx, desc.Digest) if err == nil { t.Fatalf("unexpected success opening deleted blob for read") } switch err { case distribution.ErrBlobUnknown: break default: t.Errorf("Unexpected error type getting deleted manifest: %#v", err) } // Re-upload the blob randomBlob, err := ioutil.ReadAll(randomDataReader) if err != nil { t.Fatalf("Error reading all of blob %s", err.Error()) } expectedDigest := digest.FromBytes(randomBlob) simpleUpload(t, bs, randomBlob, expectedDigest) d, err = bs.Stat(ctx, expectedDigest) if err != nil { t.Errorf("unexpected error stat-ing blob") } if d.Digest != expectedDigest { t.Errorf("Mismatching digest with restored blob") } _, err = bs.Open(ctx, expectedDigest) if err != nil { t.Errorf("Unexpected error opening blob") } // Reuse state to test delete with a delete-disabled registry registry, err = NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err = registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } bs = repository.Blobs(ctx) err = bs.Delete(ctx, desc.Digest) if err == nil { t.Errorf("Unexpected success deleting while disabled") } } // TestSimpleBlobRead just creates a simple blob file and ensures that basic // open, read, seek, read works. More specific edge cases should be covered in // other tests. func TestSimpleBlobRead(t *testing.T) { ctx := context.Background() imageName, _ := reference.ParseNamed("foo/bar") driver := testdriver.New() registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err := registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } bs := repository.Blobs(ctx) randomLayerReader, dgst, err := testutil.CreateRandomTarFile() // TODO(stevvooe): Consider using just a random string. if err != nil { t.Fatalf("error creating random data: %v", err) } // Test for existence. desc, err := bs.Stat(ctx, dgst) if err != distribution.ErrBlobUnknown { t.Fatalf("expected not found error when testing for existence: %v", err) } rc, err := bs.Open(ctx, dgst) if err != distribution.ErrBlobUnknown { t.Fatalf("expected not found error when opening non-existent blob: %v", err) } randomLayerSize, err := seekerSize(randomLayerReader) if err != nil { t.Fatalf("error getting seeker size for random layer: %v", err) } descBefore := distribution.Descriptor{Digest: dgst, MediaType: "application/octet-stream", Size: randomLayerSize} t.Logf("desc: %v", descBefore) desc, err = addBlob(ctx, bs, descBefore, randomLayerReader) if err != nil { t.Fatalf("error adding blob to blobservice: %v", err) } if desc.Size != randomLayerSize { t.Fatalf("committed blob has incorrect length: %v != %v", desc.Size, randomLayerSize) } rc, err = bs.Open(ctx, desc.Digest) // note that we are opening with original digest. if err != nil { t.Fatalf("error opening blob with %v: %v", dgst, err) } defer rc.Close() // Now check the sha digest and ensure its the same h := sha256.New() nn, err := io.Copy(h, rc) if err != nil { t.Fatalf("unexpected error copying to hash: %v", err) } if nn != randomLayerSize { t.Fatalf("stored incorrect number of bytes in blob: %d != %d", nn, randomLayerSize) } sha256Digest := digest.NewDigest("sha256", h) if sha256Digest != desc.Digest { t.Fatalf("fetched digest does not match: %q != %q", sha256Digest, desc.Digest) } // Now seek back the blob, read the whole thing and check against randomLayerData offset, err := rc.Seek(0, os.SEEK_SET) if err != nil { t.Fatalf("error seeking blob: %v", err) } if offset != 0 { t.Fatalf("seek failed: expected 0 offset, got %d", offset) } p, err := ioutil.ReadAll(rc) if err != nil { t.Fatalf("error reading all of blob: %v", err) } if len(p) != int(randomLayerSize) { t.Fatalf("blob data read has different length: %v != %v", len(p), randomLayerSize) } // Reset the randomLayerReader and read back the buffer _, err = randomLayerReader.Seek(0, os.SEEK_SET) if err != nil { t.Fatalf("error resetting layer reader: %v", err) } randomLayerData, err := ioutil.ReadAll(randomLayerReader) if err != nil { t.Fatalf("random layer read failed: %v", err) } if !bytes.Equal(p, randomLayerData) { t.Fatalf("layer data not equal") } } // TestBlobMount covers the blob mount process, exercising common // error paths that might be seen during a mount. func TestBlobMount(t *testing.T) { randomDataReader, dgst, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("error creating random reader: %v", err) } ctx := context.Background() imageName, _ := reference.ParseNamed("foo/bar") sourceImageName, _ := reference.ParseNamed("foo/source") driver := testdriver.New() registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err := registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } sourceRepository, err := registry.Repository(ctx, sourceImageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } sbs := sourceRepository.Blobs(ctx) blobUpload, err := sbs.Create(ctx) if err != nil { t.Fatalf("unexpected error starting layer upload: %s", err) } // Get the size of our random tarfile randomDataSize, err := seekerSize(randomDataReader) if err != nil { t.Fatalf("error getting seeker size of random data: %v", err) } nn, err := io.Copy(blobUpload, randomDataReader) if err != nil { t.Fatalf("unexpected error uploading layer data: %v", err) } desc, err := blobUpload.Commit(ctx, distribution.Descriptor{Digest: dgst}) if err != nil { t.Fatalf("unexpected error finishing layer upload: %v", err) } // Test for existence. statDesc, err := sbs.Stat(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error checking for existence: %v, %#v", err, sbs) } if !reflect.DeepEqual(statDesc, desc) { t.Fatalf("descriptors not equal: %v != %v", statDesc, desc) } bs := repository.Blobs(ctx) // Test destination for existence. statDesc, err = bs.Stat(ctx, desc.Digest) if err == nil { t.Fatalf("unexpected non-error stating unmounted blob: %v", desc) } canonicalRef, err := reference.WithDigest(sourceRepository.Named(), desc.Digest) if err != nil { t.Fatal(err) } bw, err := bs.Create(ctx, WithMountFrom(canonicalRef)) if bw != nil { t.Fatal("unexpected blobwriter returned from Create call, should mount instead") } ebm, ok := err.(distribution.ErrBlobMounted) if !ok { t.Fatalf("unexpected error mounting layer: %v", err) } if !reflect.DeepEqual(ebm.Descriptor, desc) { t.Fatalf("descriptors not equal: %v != %v", ebm.Descriptor, desc) } // Test for existence. statDesc, err = bs.Stat(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error checking for existence: %v, %#v", err, bs) } if !reflect.DeepEqual(statDesc, desc) { t.Fatalf("descriptors not equal: %v != %v", statDesc, desc) } rc, err := bs.Open(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error opening blob for read: %v", err) } defer rc.Close() h := sha256.New() nn, err = io.Copy(h, rc) if err != nil { t.Fatalf("error reading layer: %v", err) } if nn != randomDataSize { t.Fatalf("incorrect read length") } if digest.NewDigest("sha256", h) != dgst { t.Fatalf("unexpected digest from uploaded layer: %q != %q", digest.NewDigest("sha256", h), dgst) } // Delete the blob from the source repo err = sbs.Delete(ctx, desc.Digest) if err != nil { t.Fatalf("Unexpected error deleting blob") } d, err := bs.Stat(ctx, desc.Digest) if err != nil { t.Fatalf("unexpected error stating blob deleted from source repository: %v", err) } d, err = sbs.Stat(ctx, desc.Digest) if err == nil { t.Fatalf("unexpected non-error stating deleted blob: %v", d) } switch err { case distribution.ErrBlobUnknown: break default: t.Errorf("Unexpected error type stat-ing deleted manifest: %#v", err) } // Delete the blob from the dest repo err = bs.Delete(ctx, desc.Digest) if err != nil { t.Fatalf("Unexpected error deleting blob") } d, err = bs.Stat(ctx, desc.Digest) if err == nil { t.Fatalf("unexpected non-error stating deleted blob: %v", d) } switch err { case distribution.ErrBlobUnknown: break default: t.Errorf("Unexpected error type stat-ing deleted manifest: %#v", err) } } // TestLayerUploadZeroLength uploads zero-length func TestLayerUploadZeroLength(t *testing.T) { ctx := context.Background() imageName, _ := reference.ParseNamed("foo/bar") driver := testdriver.New() registry, err := NewRegistry(ctx, driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repository, err := registry.Repository(ctx, imageName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } bs := repository.Blobs(ctx) simpleUpload(t, bs, []byte{}, digest.DigestSha256EmptyTar) } func simpleUpload(t *testing.T, bs distribution.BlobIngester, blob []byte, expectedDigest digest.Digest) { ctx := context.Background() wr, err := bs.Create(ctx) if err != nil { t.Fatalf("unexpected error starting upload: %v", err) } nn, err := io.Copy(wr, bytes.NewReader(blob)) if err != nil { t.Fatalf("error copying into blob writer: %v", err) } if nn != 0 { t.Fatalf("unexpected number of bytes copied: %v > 0", nn) } dgst, err := digest.FromReader(bytes.NewReader(blob)) if err != nil { t.Fatalf("error getting digest: %v", err) } if dgst != expectedDigest { // sanity check on zero digest t.Fatalf("digest not as expected: %v != %v", dgst, expectedDigest) } desc, err := wr.Commit(ctx, distribution.Descriptor{Digest: dgst}) if err != nil { t.Fatalf("unexpected error committing write: %v", err) } if desc.Digest != dgst { t.Fatalf("unexpected digest: %v != %v", desc.Digest, dgst) } } // seekerSize seeks to the end of seeker, checks the size and returns it to // the original state, returning the size. The state of the seeker should be // treated as unknown if an error is returned. func seekerSize(seeker io.ReadSeeker) (int64, error) { current, err := seeker.Seek(0, os.SEEK_CUR) if err != nil { return 0, err } end, err := seeker.Seek(0, os.SEEK_END) if err != nil { return 0, err } resumed, err := seeker.Seek(current, os.SEEK_SET) if err != nil { return 0, err } if resumed != current { return 0, fmt.Errorf("error returning seeker to original state, could not seek back to original location") } return end, nil } // addBlob simply consumes the reader and inserts into the blob service, // returning a descriptor on success. func addBlob(ctx context.Context, bs distribution.BlobIngester, desc distribution.Descriptor, rd io.Reader) (distribution.Descriptor, error) { wr, err := bs.Create(ctx) if err != nil { return distribution.Descriptor{}, err } defer wr.Cancel(ctx) if nn, err := io.Copy(wr, rd); err != nil { return distribution.Descriptor{}, err } else if nn != desc.Size { return distribution.Descriptor{}, fmt.Errorf("incorrect number of bytes copied: %v != %v", nn, desc.Size) } return wr.Commit(ctx, desc) } docker-registry-2.6.2~ds1/registry/storage/blobcachemetrics.go000066400000000000000000000030711313450123100245720ustar00rootroot00000000000000package storage import ( "expvar" "sync/atomic" "github.com/docker/distribution/registry/storage/cache" ) type blobStatCollector struct { metrics cache.Metrics } func (bsc *blobStatCollector) Hit() { atomic.AddUint64(&bsc.metrics.Requests, 1) atomic.AddUint64(&bsc.metrics.Hits, 1) } func (bsc *blobStatCollector) Miss() { atomic.AddUint64(&bsc.metrics.Requests, 1) atomic.AddUint64(&bsc.metrics.Misses, 1) } func (bsc *blobStatCollector) Metrics() cache.Metrics { return bsc.metrics } // blobStatterCacheMetrics keeps track of cache metrics for blob descriptor // cache requests. Note this is kept globally and made available via expvar. // For more detailed metrics, its recommend to instrument a particular cache // implementation. var blobStatterCacheMetrics cache.MetricsTracker = &blobStatCollector{} func init() { registry := expvar.Get("registry") if registry == nil { registry = expvar.NewMap("registry") } cache := registry.(*expvar.Map).Get("cache") if cache == nil { cache = &expvar.Map{} cache.(*expvar.Map).Init() registry.(*expvar.Map).Set("cache", cache) } storage := cache.(*expvar.Map).Get("storage") if storage == nil { storage = &expvar.Map{} storage.(*expvar.Map).Init() cache.(*expvar.Map).Set("storage", storage) } storage.(*expvar.Map).Set("blobdescriptor", expvar.Func(func() interface{} { // no need for synchronous access: the increments are atomic and // during reading, we don't care if the data is up to date. The // numbers will always *eventually* be reported correctly. return blobStatterCacheMetrics })) } docker-registry-2.6.2~ds1/registry/storage/blobserver.go000066400000000000000000000041571313450123100234540ustar00rootroot00000000000000package storage import ( "fmt" "net/http" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/storage/driver" ) // TODO(stevvooe): This should configurable in the future. const blobCacheControlMaxAge = 365 * 24 * time.Hour // blobServer simply serves blobs from a driver instance using a path function // to identify paths and a descriptor service to fill in metadata. type blobServer struct { driver driver.StorageDriver statter distribution.BlobStatter pathFn func(dgst digest.Digest) (string, error) redirect bool // allows disabling URLFor redirects } func (bs *blobServer) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { desc, err := bs.statter.Stat(ctx, dgst) if err != nil { return err } path, err := bs.pathFn(desc.Digest) if err != nil { return err } if bs.redirect { redirectURL, err := bs.driver.URLFor(ctx, path, map[string]interface{}{"method": r.Method}) switch err.(type) { case nil: // Redirect to storage URL. http.Redirect(w, r, redirectURL, http.StatusTemporaryRedirect) return err case driver.ErrUnsupportedMethod: // Fallback to serving the content directly. default: // Some unexpected error. return err } } br, err := newFileReader(ctx, bs.driver, path, desc.Size) if err != nil { return err } defer br.Close() w.Header().Set("ETag", fmt.Sprintf(`"%s"`, desc.Digest)) // If-None-Match handled by ServeContent w.Header().Set("Cache-Control", fmt.Sprintf("max-age=%.f", blobCacheControlMaxAge.Seconds())) if w.Header().Get("Docker-Content-Digest") == "" { w.Header().Set("Docker-Content-Digest", desc.Digest.String()) } if w.Header().Get("Content-Type") == "" { // Set the content type if not already set. w.Header().Set("Content-Type", desc.MediaType) } if w.Header().Get("Content-Length") == "" { // Set the content length if not already set. w.Header().Set("Content-Length", fmt.Sprint(desc.Size)) } http.ServeContent(w, r, desc.Digest.String(), time.Time{}, br) return nil } docker-registry-2.6.2~ds1/registry/storage/blobstore.go000066400000000000000000000143441313450123100233010ustar00rootroot00000000000000package storage import ( "path" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/storage/driver" ) // blobStore implements the read side of the blob store interface over a // driver without enforcing per-repository membership. This object is // intentionally a leaky abstraction, providing utility methods that support // creating and traversing backend links. type blobStore struct { driver driver.StorageDriver statter distribution.BlobStatter } var _ distribution.BlobProvider = &blobStore{} // Get implements the BlobReadService.Get call. func (bs *blobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { bp, err := bs.path(dgst) if err != nil { return nil, err } p, err := getContent(ctx, bs.driver, bp) if err != nil { switch err.(type) { case driver.PathNotFoundError: return nil, distribution.ErrBlobUnknown } return nil, err } return p, nil } func (bs *blobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { desc, err := bs.statter.Stat(ctx, dgst) if err != nil { return nil, err } path, err := bs.path(desc.Digest) if err != nil { return nil, err } return newFileReader(ctx, bs.driver, path, desc.Size) } // Put stores the content p in the blob store, calculating the digest. If the // content is already present, only the digest will be returned. This should // only be used for small objects, such as manifests. This implemented as a convenience for other Put implementations func (bs *blobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { dgst := digest.FromBytes(p) desc, err := bs.statter.Stat(ctx, dgst) if err == nil { // content already present return desc, nil } else if err != distribution.ErrBlobUnknown { context.GetLogger(ctx).Errorf("blobStore: error stating content (%v): %v", dgst, err) // real error, return it return distribution.Descriptor{}, err } bp, err := bs.path(dgst) if err != nil { return distribution.Descriptor{}, err } // TODO(stevvooe): Write out mediatype here, as well. return distribution.Descriptor{ Size: int64(len(p)), // NOTE(stevvooe): The central blob store firewalls media types from // other users. The caller should look this up and override the value // for the specific repository. MediaType: "application/octet-stream", Digest: dgst, }, bs.driver.PutContent(ctx, bp, p) } func (bs *blobStore) Enumerate(ctx context.Context, ingester func(dgst digest.Digest) error) error { specPath, err := pathFor(blobsPathSpec{}) if err != nil { return err } err = Walk(ctx, bs.driver, specPath, func(fileInfo driver.FileInfo) error { // skip directories if fileInfo.IsDir() { return nil } currentPath := fileInfo.Path() // we only want to parse paths that end with /data _, fileName := path.Split(currentPath) if fileName != "data" { return nil } digest, err := digestFromPath(currentPath) if err != nil { return err } return ingester(digest) }) return err } // path returns the canonical path for the blob identified by digest. The blob // may or may not exist. func (bs *blobStore) path(dgst digest.Digest) (string, error) { bp, err := pathFor(blobDataPathSpec{ digest: dgst, }) if err != nil { return "", err } return bp, nil } // link links the path to the provided digest by writing the digest into the // target file. Caller must ensure that the blob actually exists. func (bs *blobStore) link(ctx context.Context, path string, dgst digest.Digest) error { // The contents of the "link" file are the exact string contents of the // digest, which is specified in that package. return bs.driver.PutContent(ctx, path, []byte(dgst)) } // readlink returns the linked digest at path. func (bs *blobStore) readlink(ctx context.Context, path string) (digest.Digest, error) { content, err := bs.driver.GetContent(ctx, path) if err != nil { return "", err } linked, err := digest.ParseDigest(string(content)) if err != nil { return "", err } return linked, nil } // resolve reads the digest link at path and returns the blob store path. func (bs *blobStore) resolve(ctx context.Context, path string) (string, error) { dgst, err := bs.readlink(ctx, path) if err != nil { return "", err } return bs.path(dgst) } type blobStatter struct { driver driver.StorageDriver } var _ distribution.BlobDescriptorService = &blobStatter{} // Stat implements BlobStatter.Stat by returning the descriptor for the blob // in the main blob store. If this method returns successfully, there is // strong guarantee that the blob exists and is available. func (bs *blobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { path, err := pathFor(blobDataPathSpec{ digest: dgst, }) if err != nil { return distribution.Descriptor{}, err } fi, err := bs.driver.Stat(ctx, path) if err != nil { switch err := err.(type) { case driver.PathNotFoundError: return distribution.Descriptor{}, distribution.ErrBlobUnknown default: return distribution.Descriptor{}, err } } if fi.IsDir() { // NOTE(stevvooe): This represents a corruption situation. Somehow, we // calculated a blob path and then detected a directory. We log the // error and then error on the side of not knowing about the blob. context.GetLogger(ctx).Warnf("blob path should not be a directory: %q", path) return distribution.Descriptor{}, distribution.ErrBlobUnknown } // TODO(stevvooe): Add method to resolve the mediatype. We can store and // cache a "global" media type for the blob, even if a specific repo has a // mediatype that overrides the main one. return distribution.Descriptor{ Size: fi.Size(), // NOTE(stevvooe): The central blob store firewalls media types from // other users. The caller should look this up and override the value // for the specific repository. MediaType: "application/octet-stream", Digest: dgst, }, nil } func (bs *blobStatter) Clear(ctx context.Context, dgst digest.Digest) error { return distribution.ErrUnsupported } func (bs *blobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { return distribution.ErrUnsupported } docker-registry-2.6.2~ds1/registry/storage/blobwriter.go000066400000000000000000000265311313450123100234620ustar00rootroot00000000000000package storage import ( "errors" "fmt" "io" "path" "time" "github.com/Sirupsen/logrus" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" storagedriver "github.com/docker/distribution/registry/storage/driver" ) var ( errResumableDigestNotAvailable = errors.New("resumable digest not available") ) // blobWriter is used to control the various aspects of resumable // blob upload. type blobWriter struct { ctx context.Context blobStore *linkedBlobStore id string startedAt time.Time digester digest.Digester written int64 // track the contiguous write fileWriter storagedriver.FileWriter driver storagedriver.StorageDriver path string resumableDigestEnabled bool committed bool } var _ distribution.BlobWriter = &blobWriter{} // ID returns the identifier for this upload. func (bw *blobWriter) ID() string { return bw.id } func (bw *blobWriter) StartedAt() time.Time { return bw.startedAt } // Commit marks the upload as completed, returning a valid descriptor. The // final size and digest are checked against the first descriptor provided. func (bw *blobWriter) Commit(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) { context.GetLogger(ctx).Debug("(*blobWriter).Commit") if err := bw.fileWriter.Commit(); err != nil { return distribution.Descriptor{}, err } bw.Close() desc.Size = bw.Size() canonical, err := bw.validateBlob(ctx, desc) if err != nil { return distribution.Descriptor{}, err } if err := bw.moveBlob(ctx, canonical); err != nil { return distribution.Descriptor{}, err } if err := bw.blobStore.linkBlob(ctx, canonical, desc.Digest); err != nil { return distribution.Descriptor{}, err } if err := bw.removeResources(ctx); err != nil { return distribution.Descriptor{}, err } err = bw.blobStore.blobAccessController.SetDescriptor(ctx, canonical.Digest, canonical) if err != nil { return distribution.Descriptor{}, err } bw.committed = true return canonical, nil } // Cancel the blob upload process, releasing any resources associated with // the writer and canceling the operation. func (bw *blobWriter) Cancel(ctx context.Context) error { context.GetLogger(ctx).Debug("(*blobWriter).Cancel") if err := bw.fileWriter.Cancel(); err != nil { return err } if err := bw.Close(); err != nil { context.GetLogger(ctx).Errorf("error closing blobwriter: %s", err) } if err := bw.removeResources(ctx); err != nil { return err } return nil } func (bw *blobWriter) Size() int64 { return bw.fileWriter.Size() } func (bw *blobWriter) Write(p []byte) (int, error) { // Ensure that the current write offset matches how many bytes have been // written to the digester. If not, we need to update the digest state to // match the current write position. if err := bw.resumeDigest(bw.blobStore.ctx); err != nil && err != errResumableDigestNotAvailable { return 0, err } n, err := io.MultiWriter(bw.fileWriter, bw.digester.Hash()).Write(p) bw.written += int64(n) return n, err } func (bw *blobWriter) ReadFrom(r io.Reader) (n int64, err error) { // Ensure that the current write offset matches how many bytes have been // written to the digester. If not, we need to update the digest state to // match the current write position. if err := bw.resumeDigest(bw.blobStore.ctx); err != nil && err != errResumableDigestNotAvailable { return 0, err } nn, err := io.Copy(io.MultiWriter(bw.fileWriter, bw.digester.Hash()), r) bw.written += nn return nn, err } func (bw *blobWriter) Close() error { if bw.committed { return errors.New("blobwriter close after commit") } if err := bw.storeHashState(bw.blobStore.ctx); err != nil && err != errResumableDigestNotAvailable { return err } return bw.fileWriter.Close() } // validateBlob checks the data against the digest, returning an error if it // does not match. The canonical descriptor is returned. func (bw *blobWriter) validateBlob(ctx context.Context, desc distribution.Descriptor) (distribution.Descriptor, error) { var ( verified, fullHash bool canonical digest.Digest ) if desc.Digest == "" { // if no descriptors are provided, we have nothing to validate // against. We don't really want to support this for the registry. return distribution.Descriptor{}, distribution.ErrBlobInvalidDigest{ Reason: fmt.Errorf("cannot validate against empty digest"), } } var size int64 // Stat the on disk file if fi, err := bw.driver.Stat(ctx, bw.path); err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: // NOTE(stevvooe): We really don't care if the file is // not actually present for the reader. We now assume // that the desc length is zero. desc.Size = 0 default: // Any other error we want propagated up the stack. return distribution.Descriptor{}, err } } else { if fi.IsDir() { return distribution.Descriptor{}, fmt.Errorf("unexpected directory at upload location %q", bw.path) } size = fi.Size() } if desc.Size > 0 { if desc.Size != size { return distribution.Descriptor{}, distribution.ErrBlobInvalidLength } } else { // if provided 0 or negative length, we can assume caller doesn't know or // care about length. desc.Size = size } // TODO(stevvooe): This section is very meandering. Need to be broken down // to be a lot more clear. if err := bw.resumeDigest(ctx); err == nil { canonical = bw.digester.Digest() if canonical.Algorithm() == desc.Digest.Algorithm() { // Common case: client and server prefer the same canonical digest // algorithm - currently SHA256. verified = desc.Digest == canonical } else { // The client wants to use a different digest algorithm. They'll just // have to be patient and wait for us to download and re-hash the // uploaded content using that digest algorithm. fullHash = true } } else if err == errResumableDigestNotAvailable { // Not using resumable digests, so we need to hash the entire layer. fullHash = true } else { return distribution.Descriptor{}, err } if fullHash { // a fantastic optimization: if the the written data and the size are // the same, we don't need to read the data from the backend. This is // because we've written the entire file in the lifecycle of the // current instance. if bw.written == size && digest.Canonical == desc.Digest.Algorithm() { canonical = bw.digester.Digest() verified = desc.Digest == canonical } // If the check based on size fails, we fall back to the slowest of // paths. We may be able to make the size-based check a stronger // guarantee, so this may be defensive. if !verified { digester := digest.Canonical.New() digestVerifier, err := digest.NewDigestVerifier(desc.Digest) if err != nil { return distribution.Descriptor{}, err } // Read the file from the backend driver and validate it. fr, err := newFileReader(ctx, bw.driver, bw.path, desc.Size) if err != nil { return distribution.Descriptor{}, err } defer fr.Close() tr := io.TeeReader(fr, digester.Hash()) if _, err := io.Copy(digestVerifier, tr); err != nil { return distribution.Descriptor{}, err } canonical = digester.Digest() verified = digestVerifier.Verified() } } if !verified { context.GetLoggerWithFields(ctx, map[interface{}]interface{}{ "canonical": canonical, "provided": desc.Digest, }, "canonical", "provided"). Errorf("canonical digest does match provided digest") return distribution.Descriptor{}, distribution.ErrBlobInvalidDigest{ Digest: desc.Digest, Reason: fmt.Errorf("content does not match digest"), } } // update desc with canonical hash desc.Digest = canonical if desc.MediaType == "" { desc.MediaType = "application/octet-stream" } return desc, nil } // moveBlob moves the data into its final, hash-qualified destination, // identified by dgst. The layer should be validated before commencing the // move. func (bw *blobWriter) moveBlob(ctx context.Context, desc distribution.Descriptor) error { blobPath, err := pathFor(blobDataPathSpec{ digest: desc.Digest, }) if err != nil { return err } // Check for existence if _, err := bw.blobStore.driver.Stat(ctx, blobPath); err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: break // ensure that it doesn't exist. default: return err } } else { // If the path exists, we can assume that the content has already // been uploaded, since the blob storage is content-addressable. // While it may be corrupted, detection of such corruption belongs // elsewhere. return nil } // If no data was received, we may not actually have a file on disk. Check // the size here and write a zero-length file to blobPath if this is the // case. For the most part, this should only ever happen with zero-length // tars. if _, err := bw.blobStore.driver.Stat(ctx, bw.path); err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: // HACK(stevvooe): This is slightly dangerous: if we verify above, // get a hash, then the underlying file is deleted, we risk moving // a zero-length blob into a nonzero-length blob location. To // prevent this horrid thing, we employ the hack of only allowing // to this happen for the digest of an empty tar. if desc.Digest == digest.DigestSha256EmptyTar { return bw.blobStore.driver.PutContent(ctx, blobPath, []byte{}) } // We let this fail during the move below. logrus. WithField("upload.id", bw.ID()). WithField("digest", desc.Digest).Warnf("attempted to move zero-length content with non-zero digest") default: return err // unrelated error } } // TODO(stevvooe): We should also write the mediatype when executing this move. return bw.blobStore.driver.Move(ctx, bw.path, blobPath) } // removeResources should clean up all resources associated with the upload // instance. An error will be returned if the clean up cannot proceed. If the // resources are already not present, no error will be returned. func (bw *blobWriter) removeResources(ctx context.Context) error { dataPath, err := pathFor(uploadDataPathSpec{ name: bw.blobStore.repository.Named().Name(), id: bw.id, }) if err != nil { return err } // Resolve and delete the containing directory, which should include any // upload related files. dirPath := path.Dir(dataPath) if err := bw.blobStore.driver.Delete(ctx, dirPath); err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: break // already gone! default: // This should be uncommon enough such that returning an error // should be okay. At this point, the upload should be mostly // complete, but perhaps the backend became unaccessible. context.GetLogger(ctx).Errorf("unable to delete layer upload resources %q: %v", dirPath, err) return err } } return nil } func (bw *blobWriter) Reader() (io.ReadCloser, error) { // todo(richardscothern): Change to exponential backoff, i=0.5, e=2, n=4 try := 1 for try <= 5 { _, err := bw.driver.Stat(bw.ctx, bw.path) if err == nil { break } switch err.(type) { case storagedriver.PathNotFoundError: context.GetLogger(bw.ctx).Debugf("Nothing found on try %d, sleeping...", try) time.Sleep(1 * time.Second) try++ default: return nil, err } } readCloser, err := bw.driver.Reader(bw.ctx, bw.path, 0) if err != nil { return nil, err } return readCloser, nil } docker-registry-2.6.2~ds1/registry/storage/blobwriter_nonresumable.go000066400000000000000000000007051313450123100262270ustar00rootroot00000000000000// +build noresumabledigest package storage import ( "github.com/docker/distribution/context" ) // resumeHashAt is a noop when resumable digest support is disabled. func (bw *blobWriter) resumeDigest(ctx context.Context) error { return errResumableDigestNotAvailable } // storeHashState is a noop when resumable digest support is disabled. func (bw *blobWriter) storeHashState(ctx context.Context) error { return errResumableDigestNotAvailable } docker-registry-2.6.2~ds1/registry/storage/blobwriter_resumable.go000066400000000000000000000067521313450123100255240ustar00rootroot00000000000000// +build !noresumabledigest package storage import ( "fmt" "path" "strconv" "github.com/Sirupsen/logrus" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/stevvooe/resumable" // register resumable hashes with import _ "github.com/stevvooe/resumable/sha256" _ "github.com/stevvooe/resumable/sha512" ) // resumeDigest attempts to restore the state of the internal hash function // by loading the most recent saved hash state equal to the current size of the blob. func (bw *blobWriter) resumeDigest(ctx context.Context) error { if !bw.resumableDigestEnabled { return errResumableDigestNotAvailable } h, ok := bw.digester.Hash().(resumable.Hash) if !ok { return errResumableDigestNotAvailable } offset := bw.fileWriter.Size() if offset == int64(h.Len()) { // State of digester is already at the requested offset. return nil } // List hash states from storage backend. var hashStateMatch hashStateEntry hashStates, err := bw.getStoredHashStates(ctx) if err != nil { return fmt.Errorf("unable to get stored hash states with offset %d: %s", offset, err) } // Find the highest stored hashState with offset equal to // the requested offset. for _, hashState := range hashStates { if hashState.offset == offset { hashStateMatch = hashState break // Found an exact offset match. } } if hashStateMatch.offset == 0 { // No need to load any state, just reset the hasher. h.Reset() } else { storedState, err := bw.driver.GetContent(ctx, hashStateMatch.path) if err != nil { return err } if err = h.Restore(storedState); err != nil { return err } } // Mind the gap. if gapLen := offset - int64(h.Len()); gapLen > 0 { return errResumableDigestNotAvailable } return nil } type hashStateEntry struct { offset int64 path string } // getStoredHashStates returns a slice of hashStateEntries for this upload. func (bw *blobWriter) getStoredHashStates(ctx context.Context) ([]hashStateEntry, error) { uploadHashStatePathPrefix, err := pathFor(uploadHashStatePathSpec{ name: bw.blobStore.repository.Named().String(), id: bw.id, alg: bw.digester.Digest().Algorithm(), list: true, }) if err != nil { return nil, err } paths, err := bw.blobStore.driver.List(ctx, uploadHashStatePathPrefix) if err != nil { if _, ok := err.(storagedriver.PathNotFoundError); !ok { return nil, err } // Treat PathNotFoundError as no entries. paths = nil } hashStateEntries := make([]hashStateEntry, 0, len(paths)) for _, p := range paths { pathSuffix := path.Base(p) // The suffix should be the offset. offset, err := strconv.ParseInt(pathSuffix, 0, 64) if err != nil { logrus.Errorf("unable to parse offset from upload state path %q: %s", p, err) } hashStateEntries = append(hashStateEntries, hashStateEntry{offset: offset, path: p}) } return hashStateEntries, nil } func (bw *blobWriter) storeHashState(ctx context.Context) error { if !bw.resumableDigestEnabled { return errResumableDigestNotAvailable } h, ok := bw.digester.Hash().(resumable.Hash) if !ok { return errResumableDigestNotAvailable } uploadHashStatePath, err := pathFor(uploadHashStatePathSpec{ name: bw.blobStore.repository.Named().String(), id: bw.id, alg: bw.digester.Digest().Algorithm(), offset: int64(h.Len()), }) if err != nil { return err } hashState, err := h.State() if err != nil { return err } return bw.driver.PutContent(ctx, uploadHashStatePath, hashState) } docker-registry-2.6.2~ds1/registry/storage/cache/000077500000000000000000000000001313450123100220145ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/cache/cache.go000066400000000000000000000016141313450123100234100ustar00rootroot00000000000000// Package cache provides facilities to speed up access to the storage // backend. package cache import ( "fmt" "github.com/docker/distribution" ) // BlobDescriptorCacheProvider provides repository scoped // BlobDescriptorService cache instances and a global descriptor cache. type BlobDescriptorCacheProvider interface { distribution.BlobDescriptorService RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) } // ValidateDescriptor provides a helper function to ensure that caches have // common criteria for admitting descriptors. func ValidateDescriptor(desc distribution.Descriptor) error { if err := desc.Digest.Validate(); err != nil { return err } if desc.Size < 0 { return fmt.Errorf("cache: invalid length in descriptor: %v < 0", desc.Size) } if desc.MediaType == "" { return fmt.Errorf("cache: empty mediatype on descriptor: %v", desc) } return nil } docker-registry-2.6.2~ds1/registry/storage/cache/cachecheck/000077500000000000000000000000001313450123100240555ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/cache/cachecheck/suite.go000066400000000000000000000140131313450123100255340ustar00rootroot00000000000000package cachecheck import ( "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/storage/cache" ) // CheckBlobDescriptorCache takes a cache implementation through a common set // of operations. If adding new tests, please add them here so new // implementations get the benefit. This should be used for unit tests. func CheckBlobDescriptorCache(t *testing.T, provider cache.BlobDescriptorCacheProvider) { ctx := context.Background() checkBlobDescriptorCacheEmptyRepository(ctx, t, provider) checkBlobDescriptorCacheSetAndRead(ctx, t, provider) checkBlobDescriptorCacheClear(ctx, t, provider) } func checkBlobDescriptorCacheEmptyRepository(ctx context.Context, t *testing.T, provider cache.BlobDescriptorCacheProvider) { if _, err := provider.Stat(ctx, "sha384:abc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); err != distribution.ErrBlobUnknown { t.Fatalf("expected unknown blob error with empty store: %v", err) } cache, err := provider.RepositoryScoped("") if err == nil { t.Fatalf("expected an error when asking for invalid repo") } cache, err = provider.RepositoryScoped("foo/bar") if err != nil { t.Fatalf("unexpected error getting repository: %v", err) } if err := cache.SetDescriptor(ctx, "", distribution.Descriptor{ Digest: "sha384:abc", Size: 10, MediaType: "application/octet-stream"}); err != digest.ErrDigestInvalidFormat { t.Fatalf("expected error with invalid digest: %v", err) } if err := cache.SetDescriptor(ctx, "sha384:abc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", distribution.Descriptor{ Digest: "", Size: 10, MediaType: "application/octet-stream"}); err == nil { t.Fatalf("expected error setting value on invalid descriptor") } if _, err := cache.Stat(ctx, ""); err != digest.ErrDigestInvalidFormat { t.Fatalf("expected error checking for cache item with empty digest: %v", err) } if _, err := cache.Stat(ctx, "sha384:abc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111"); err != distribution.ErrBlobUnknown { t.Fatalf("expected unknown blob error with empty repo: %v", err) } } func checkBlobDescriptorCacheSetAndRead(ctx context.Context, t *testing.T, provider cache.BlobDescriptorCacheProvider) { localDigest := digest.Digest("sha384:abc111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") expected := distribution.Descriptor{ Digest: "sha256:abc1111111111111111111111111111111111111111111111111111111111111", Size: 10, MediaType: "application/octet-stream"} cache, err := provider.RepositoryScoped("foo/bar") if err != nil { t.Fatalf("unexpected error getting scoped cache: %v", err) } if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil { t.Fatalf("error setting descriptor: %v", err) } desc, err := cache.Stat(ctx, localDigest) if err != nil { t.Fatalf("unexpected error statting fake2:abc: %v", err) } if !reflect.DeepEqual(expected, desc) { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc) } // also check that we set the canonical key ("fake:abc") desc, err = cache.Stat(ctx, localDigest) if err != nil { t.Fatalf("descriptor not returned for canonical key: %v", err) } if !reflect.DeepEqual(expected, desc) { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc) } // ensure that global gets extra descriptor mapping desc, err = provider.Stat(ctx, localDigest) if err != nil { t.Fatalf("expected blob unknown in global cache: %v, %v", err, desc) } if !reflect.DeepEqual(desc, expected) { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc) } // get at it through canonical descriptor desc, err = provider.Stat(ctx, expected.Digest) if err != nil { t.Fatalf("unexpected error checking glboal descriptor: %v", err) } if !reflect.DeepEqual(desc, expected) { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc) } // now, we set the repo local mediatype to something else and ensure it // doesn't get changed in the provider cache. expected.MediaType = "application/json" if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil { t.Fatalf("unexpected error setting descriptor: %v", err) } desc, err = cache.Stat(ctx, localDigest) if err != nil { t.Fatalf("unexpected error getting descriptor: %v", err) } if !reflect.DeepEqual(desc, expected) { t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected) } desc, err = provider.Stat(ctx, localDigest) if err != nil { t.Fatalf("unexpected error getting global descriptor: %v", err) } expected.MediaType = "application/octet-stream" // expect original mediatype in global if !reflect.DeepEqual(desc, expected) { t.Fatalf("unexpected descriptor: %#v != %#v", desc, expected) } } func checkBlobDescriptorCacheClear(ctx context.Context, t *testing.T, provider cache.BlobDescriptorCacheProvider) { localDigest := digest.Digest("sha384:def111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") expected := distribution.Descriptor{ Digest: "sha256:def1111111111111111111111111111111111111111111111111111111111111", Size: 10, MediaType: "application/octet-stream"} cache, err := provider.RepositoryScoped("foo/bar") if err != nil { t.Fatalf("unexpected error getting scoped cache: %v", err) } if err := cache.SetDescriptor(ctx, localDigest, expected); err != nil { t.Fatalf("error setting descriptor: %v", err) } desc, err := cache.Stat(ctx, localDigest) if err != nil { t.Fatalf("unexpected error statting fake2:abc: %v", err) } if !reflect.DeepEqual(expected, desc) { t.Fatalf("unexpected descriptor: %#v != %#v", expected, desc) } err = cache.Clear(ctx, localDigest) if err != nil { t.Error(err) } desc, err = cache.Stat(ctx, localDigest) if err == nil { t.Fatalf("expected error statting deleted blob: %v", err) } } docker-registry-2.6.2~ds1/registry/storage/cache/cachedblobdescriptorstore.go000066400000000000000000000051241313450123100275670ustar00rootroot00000000000000package cache import ( "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution" ) // Metrics is used to hold metric counters // related to the number of times a cache was // hit or missed. type Metrics struct { Requests uint64 Hits uint64 Misses uint64 } // MetricsTracker represents a metric tracker // which simply counts the number of hits and misses. type MetricsTracker interface { Hit() Miss() Metrics() Metrics } type cachedBlobStatter struct { cache distribution.BlobDescriptorService backend distribution.BlobDescriptorService tracker MetricsTracker } // NewCachedBlobStatter creates a new statter which prefers a cache and // falls back to a backend. func NewCachedBlobStatter(cache distribution.BlobDescriptorService, backend distribution.BlobDescriptorService) distribution.BlobDescriptorService { return &cachedBlobStatter{ cache: cache, backend: backend, } } // NewCachedBlobStatterWithMetrics creates a new statter which prefers a cache and // falls back to a backend. Hits and misses will send to the tracker. func NewCachedBlobStatterWithMetrics(cache distribution.BlobDescriptorService, backend distribution.BlobDescriptorService, tracker MetricsTracker) distribution.BlobStatter { return &cachedBlobStatter{ cache: cache, backend: backend, tracker: tracker, } } func (cbds *cachedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { desc, err := cbds.cache.Stat(ctx, dgst) if err != nil { if err != distribution.ErrBlobUnknown { context.GetLogger(ctx).Errorf("error retrieving descriptor from cache: %v", err) } goto fallback } if cbds.tracker != nil { cbds.tracker.Hit() } return desc, nil fallback: if cbds.tracker != nil { cbds.tracker.Miss() } desc, err = cbds.backend.Stat(ctx, dgst) if err != nil { return desc, err } if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil { context.GetLogger(ctx).Errorf("error adding descriptor %v to cache: %v", desc.Digest, err) } return desc, err } func (cbds *cachedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) error { err := cbds.cache.Clear(ctx, dgst) if err != nil { return err } err = cbds.backend.Clear(ctx, dgst) if err != nil { return err } return nil } func (cbds *cachedBlobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { if err := cbds.cache.SetDescriptor(ctx, dgst, desc); err != nil { context.GetLogger(ctx).Errorf("error adding descriptor %v to cache: %v", desc.Digest, err) } return nil } docker-registry-2.6.2~ds1/registry/storage/cache/memory/000077500000000000000000000000001313450123100233245ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/cache/memory/memory.go000066400000000000000000000120561313450123100251670ustar00rootroot00000000000000package memory import ( "sync" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" ) type inMemoryBlobDescriptorCacheProvider struct { global *mapBlobDescriptorCache repositories map[string]*mapBlobDescriptorCache mu sync.RWMutex } // NewInMemoryBlobDescriptorCacheProvider returns a new mapped-based cache for // storing blob descriptor data. func NewInMemoryBlobDescriptorCacheProvider() cache.BlobDescriptorCacheProvider { return &inMemoryBlobDescriptorCacheProvider{ global: newMapBlobDescriptorCache(), repositories: make(map[string]*mapBlobDescriptorCache), } } func (imbdcp *inMemoryBlobDescriptorCacheProvider) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { if _, err := reference.ParseNamed(repo); err != nil { return nil, err } imbdcp.mu.RLock() defer imbdcp.mu.RUnlock() return &repositoryScopedInMemoryBlobDescriptorCache{ repo: repo, parent: imbdcp, repository: imbdcp.repositories[repo], }, nil } func (imbdcp *inMemoryBlobDescriptorCacheProvider) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { return imbdcp.global.Stat(ctx, dgst) } func (imbdcp *inMemoryBlobDescriptorCacheProvider) Clear(ctx context.Context, dgst digest.Digest) error { return imbdcp.global.Clear(ctx, dgst) } func (imbdcp *inMemoryBlobDescriptorCacheProvider) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { _, err := imbdcp.Stat(ctx, dgst) if err == distribution.ErrBlobUnknown { if dgst.Algorithm() != desc.Digest.Algorithm() && dgst != desc.Digest { // if the digests differ, set the other canonical mapping if err := imbdcp.global.SetDescriptor(ctx, desc.Digest, desc); err != nil { return err } } // unknown, just set it return imbdcp.global.SetDescriptor(ctx, dgst, desc) } // we already know it, do nothing return err } // repositoryScopedInMemoryBlobDescriptorCache provides the request scoped // repository cache. Instances are not thread-safe but the delegated // operations are. type repositoryScopedInMemoryBlobDescriptorCache struct { repo string parent *inMemoryBlobDescriptorCacheProvider // allows lazy allocation of repo's map repository *mapBlobDescriptorCache } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { rsimbdcp.parent.mu.Lock() repo := rsimbdcp.repository rsimbdcp.parent.mu.Unlock() if repo == nil { return distribution.Descriptor{}, distribution.ErrBlobUnknown } return repo.Stat(ctx, dgst) } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) Clear(ctx context.Context, dgst digest.Digest) error { rsimbdcp.parent.mu.Lock() repo := rsimbdcp.repository rsimbdcp.parent.mu.Unlock() if repo == nil { return distribution.ErrBlobUnknown } return repo.Clear(ctx, dgst) } func (rsimbdcp *repositoryScopedInMemoryBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { rsimbdcp.parent.mu.Lock() repo := rsimbdcp.repository if repo == nil { // allocate map since we are setting it now. var ok bool // have to read back value since we may have allocated elsewhere. repo, ok = rsimbdcp.parent.repositories[rsimbdcp.repo] if !ok { repo = newMapBlobDescriptorCache() rsimbdcp.parent.repositories[rsimbdcp.repo] = repo } rsimbdcp.repository = repo } rsimbdcp.parent.mu.Unlock() if err := repo.SetDescriptor(ctx, dgst, desc); err != nil { return err } return rsimbdcp.parent.SetDescriptor(ctx, dgst, desc) } // mapBlobDescriptorCache provides a simple map-based implementation of the // descriptor cache. type mapBlobDescriptorCache struct { descriptors map[digest.Digest]distribution.Descriptor mu sync.RWMutex } var _ distribution.BlobDescriptorService = &mapBlobDescriptorCache{} func newMapBlobDescriptorCache() *mapBlobDescriptorCache { return &mapBlobDescriptorCache{ descriptors: make(map[digest.Digest]distribution.Descriptor), } } func (mbdc *mapBlobDescriptorCache) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if err := dgst.Validate(); err != nil { return distribution.Descriptor{}, err } mbdc.mu.RLock() defer mbdc.mu.RUnlock() desc, ok := mbdc.descriptors[dgst] if !ok { return distribution.Descriptor{}, distribution.ErrBlobUnknown } return desc, nil } func (mbdc *mapBlobDescriptorCache) Clear(ctx context.Context, dgst digest.Digest) error { mbdc.mu.Lock() defer mbdc.mu.Unlock() delete(mbdc.descriptors, dgst) return nil } func (mbdc *mapBlobDescriptorCache) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { if err := dgst.Validate(); err != nil { return err } if err := cache.ValidateDescriptor(desc); err != nil { return err } mbdc.mu.Lock() defer mbdc.mu.Unlock() mbdc.descriptors[dgst] = desc return nil } docker-registry-2.6.2~ds1/registry/storage/cache/memory/memory_test.go000066400000000000000000000005111313450123100262170ustar00rootroot00000000000000package memory import ( "testing" "github.com/docker/distribution/registry/storage/cache/cachecheck" ) // TestInMemoryBlobInfoCache checks the in memory implementation is working // correctly. func TestInMemoryBlobInfoCache(t *testing.T) { cachecheck.CheckBlobDescriptorCache(t, NewInMemoryBlobDescriptorCacheProvider()) } docker-registry-2.6.2~ds1/registry/storage/cache/redis/000077500000000000000000000000001313450123100231225ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/cache/redis/redis.go000066400000000000000000000206611313450123100245640ustar00rootroot00000000000000package redis import ( "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" "github.com/garyburd/redigo/redis" ) // redisBlobStatService provides an implementation of // BlobDescriptorCacheProvider based on redis. Blob descriptors are stored in // two parts. The first provide fast access to repository membership through a // redis set for each repo. The second is a redis hash keyed by the digest of // the layer, providing path, length and mediatype information. There is also // a per-repository redis hash of the blob descriptor, allowing override of // data. This is currently used to override the mediatype on a per-repository // basis. // // Note that there is no implied relationship between these two caches. The // layer may exist in one, both or none and the code must be written this way. type redisBlobDescriptorService struct { pool *redis.Pool // TODO(stevvooe): We use a pool because we don't have great control over // the cache lifecycle to manage connections. A new connection if fetched // for each operation. Once we have better lifecycle management of the // request objects, we can change this to a connection. } // NewRedisBlobDescriptorCacheProvider returns a new redis-based // BlobDescriptorCacheProvider using the provided redis connection pool. func NewRedisBlobDescriptorCacheProvider(pool *redis.Pool) cache.BlobDescriptorCacheProvider { return &redisBlobDescriptorService{ pool: pool, } } // RepositoryScoped returns the scoped cache. func (rbds *redisBlobDescriptorService) RepositoryScoped(repo string) (distribution.BlobDescriptorService, error) { if _, err := reference.ParseNamed(repo); err != nil { return nil, err } return &repositoryScopedRedisBlobDescriptorService{ repo: repo, upstream: rbds, }, nil } // Stat retrieves the descriptor data from the redis hash entry. func (rbds *redisBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if err := dgst.Validate(); err != nil { return distribution.Descriptor{}, err } conn := rbds.pool.Get() defer conn.Close() return rbds.stat(ctx, conn, dgst) } func (rbds *redisBlobDescriptorService) Clear(ctx context.Context, dgst digest.Digest) error { if err := dgst.Validate(); err != nil { return err } conn := rbds.pool.Get() defer conn.Close() // Not atomic in redis <= 2.3 reply, err := conn.Do("HDEL", rbds.blobDescriptorHashKey(dgst), "digest", "length", "mediatype") if err != nil { return err } if reply == 0 { return distribution.ErrBlobUnknown } return nil } // stat provides an internal stat call that takes a connection parameter. This // allows some internal management of the connection scope. func (rbds *redisBlobDescriptorService) stat(ctx context.Context, conn redis.Conn, dgst digest.Digest) (distribution.Descriptor, error) { reply, err := redis.Values(conn.Do("HMGET", rbds.blobDescriptorHashKey(dgst), "digest", "size", "mediatype")) if err != nil { return distribution.Descriptor{}, err } // NOTE(stevvooe): The "size" field used to be "length". We treat a // missing "size" field here as an unknown blob, which causes a cache // miss, effectively migrating the field. if len(reply) < 3 || reply[0] == nil || reply[1] == nil { // don't care if mediatype is nil return distribution.Descriptor{}, distribution.ErrBlobUnknown } var desc distribution.Descriptor if _, err := redis.Scan(reply, &desc.Digest, &desc.Size, &desc.MediaType); err != nil { return distribution.Descriptor{}, err } return desc, nil } // SetDescriptor sets the descriptor data for the given digest using a redis // hash. A hash is used here since we may store unrelated fields about a layer // in the future. func (rbds *redisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { if err := dgst.Validate(); err != nil { return err } if err := cache.ValidateDescriptor(desc); err != nil { return err } conn := rbds.pool.Get() defer conn.Close() return rbds.setDescriptor(ctx, conn, dgst, desc) } func (rbds *redisBlobDescriptorService) setDescriptor(ctx context.Context, conn redis.Conn, dgst digest.Digest, desc distribution.Descriptor) error { if _, err := conn.Do("HMSET", rbds.blobDescriptorHashKey(dgst), "digest", desc.Digest, "size", desc.Size); err != nil { return err } // Only set mediatype if not already set. if _, err := conn.Do("HSETNX", rbds.blobDescriptorHashKey(dgst), "mediatype", desc.MediaType); err != nil { return err } return nil } func (rbds *redisBlobDescriptorService) blobDescriptorHashKey(dgst digest.Digest) string { return "blobs::" + dgst.String() } type repositoryScopedRedisBlobDescriptorService struct { repo string upstream *redisBlobDescriptorService } var _ distribution.BlobDescriptorService = &repositoryScopedRedisBlobDescriptorService{} // Stat ensures that the digest is a member of the specified repository and // forwards the descriptor request to the global blob store. If the media type // differs for the repository, we override it. func (rsrbds *repositoryScopedRedisBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { if err := dgst.Validate(); err != nil { return distribution.Descriptor{}, err } conn := rsrbds.upstream.pool.Get() defer conn.Close() // Check membership to repository first member, err := redis.Bool(conn.Do("SISMEMBER", rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst)) if err != nil { return distribution.Descriptor{}, err } if !member { return distribution.Descriptor{}, distribution.ErrBlobUnknown } upstream, err := rsrbds.upstream.stat(ctx, conn, dgst) if err != nil { return distribution.Descriptor{}, err } // We allow a per repository mediatype, let's look it up here. mediatype, err := redis.String(conn.Do("HGET", rsrbds.blobDescriptorHashKey(dgst), "mediatype")) if err != nil { return distribution.Descriptor{}, err } if mediatype != "" { upstream.MediaType = mediatype } return upstream, nil } // Clear removes the descriptor from the cache and forwards to the upstream descriptor store func (rsrbds *repositoryScopedRedisBlobDescriptorService) Clear(ctx context.Context, dgst digest.Digest) error { if err := dgst.Validate(); err != nil { return err } conn := rsrbds.upstream.pool.Get() defer conn.Close() // Check membership to repository first member, err := redis.Bool(conn.Do("SISMEMBER", rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst)) if err != nil { return err } if !member { return distribution.ErrBlobUnknown } return rsrbds.upstream.Clear(ctx, dgst) } func (rsrbds *repositoryScopedRedisBlobDescriptorService) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { if err := dgst.Validate(); err != nil { return err } if err := cache.ValidateDescriptor(desc); err != nil { return err } if dgst != desc.Digest { if dgst.Algorithm() == desc.Digest.Algorithm() { return fmt.Errorf("redis cache: digest for descriptors differ but algorthim does not: %q != %q", dgst, desc.Digest) } } conn := rsrbds.upstream.pool.Get() defer conn.Close() return rsrbds.setDescriptor(ctx, conn, dgst, desc) } func (rsrbds *repositoryScopedRedisBlobDescriptorService) setDescriptor(ctx context.Context, conn redis.Conn, dgst digest.Digest, desc distribution.Descriptor) error { if _, err := conn.Do("SADD", rsrbds.repositoryBlobSetKey(rsrbds.repo), dgst); err != nil { return err } if err := rsrbds.upstream.setDescriptor(ctx, conn, dgst, desc); err != nil { return err } // Override repository mediatype. if _, err := conn.Do("HSET", rsrbds.blobDescriptorHashKey(dgst), "mediatype", desc.MediaType); err != nil { return err } // Also set the values for the primary descriptor, if they differ by // algorithm (ie sha256 vs sha512). if desc.Digest != "" && dgst != desc.Digest && dgst.Algorithm() != desc.Digest.Algorithm() { if err := rsrbds.setDescriptor(ctx, conn, desc.Digest, desc); err != nil { return err } } return nil } func (rsrbds *repositoryScopedRedisBlobDescriptorService) blobDescriptorHashKey(dgst digest.Digest) string { return "repository::" + rsrbds.repo + "::blobs::" + dgst.String() } func (rsrbds *repositoryScopedRedisBlobDescriptorService) repositoryBlobSetKey(repo string) string { return "repository::" + rsrbds.repo + "::blobs" } docker-registry-2.6.2~ds1/registry/storage/cache/redis/redis_test.go000066400000000000000000000024721313450123100256230ustar00rootroot00000000000000package redis import ( "flag" "os" "testing" "time" "github.com/docker/distribution/registry/storage/cache/cachecheck" "github.com/garyburd/redigo/redis" ) var redisAddr string func init() { flag.StringVar(&redisAddr, "test.registry.storage.cache.redis.addr", "", "configure the address of a test instance of redis") } // TestRedisLayerInfoCache exercises a live redis instance using the cache // implementation. func TestRedisBlobDescriptorCacheProvider(t *testing.T) { if redisAddr == "" { // fallback to an environement variable redisAddr = os.Getenv("TEST_REGISTRY_STORAGE_CACHE_REDIS_ADDR") } if redisAddr == "" { // skip if still not set t.Skip("please set -test.registry.storage.cache.redis.addr to test layer info cache against redis") } pool := &redis.Pool{ Dial: func() (redis.Conn, error) { return redis.Dial("tcp", redisAddr) }, MaxIdle: 1, MaxActive: 2, TestOnBorrow: func(c redis.Conn, t time.Time) error { _, err := c.Do("PING") return err }, Wait: false, // if a connection is not avialable, proceed without cache. } // Clear the database conn := pool.Get() if _, err := conn.Do("FLUSHDB"); err != nil { t.Fatalf("unexpected error flushing redis db: %v", err) } conn.Close() cachecheck.CheckBlobDescriptorCache(t, NewRedisBlobDescriptorCacheProvider(pool)) } docker-registry-2.6.2~ds1/registry/storage/catalog.go000066400000000000000000000070061313450123100227150ustar00rootroot00000000000000package storage import ( "errors" "io" "path" "strings" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage/driver" ) // errFinishedWalk signals an early exit to the walk when the current query // is satisfied. var errFinishedWalk = errors.New("finished walk") // Returns a list, or partial list, of repositories in the registry. // Because it's a quite expensive operation, it should only be used when building up // an initial set of repositories. func (reg *registry) Repositories(ctx context.Context, repos []string, last string) (n int, err error) { var foundRepos []string if len(repos) == 0 { return 0, errors.New("no space in slice") } root, err := pathFor(repositoriesRootPathSpec{}) if err != nil { return 0, err } err = Walk(ctx, reg.blobStore.driver, root, func(fileInfo driver.FileInfo) error { err := handleRepository(fileInfo, root, last, func(repoPath string) error { foundRepos = append(foundRepos, repoPath) return nil }) if err != nil { return err } // if we've filled our array, no need to walk any further if len(foundRepos) == len(repos) { return errFinishedWalk } return nil }) n = copy(repos, foundRepos) switch err { case nil: // nil means that we completed walk and didn't fill buffer. No more // records are available. err = io.EOF case errFinishedWalk: // more records are available. err = nil } return n, err } // Enumerate applies ingester to each repository func (reg *registry) Enumerate(ctx context.Context, ingester func(string) error) error { root, err := pathFor(repositoriesRootPathSpec{}) if err != nil { return err } err = Walk(ctx, reg.blobStore.driver, root, func(fileInfo driver.FileInfo) error { return handleRepository(fileInfo, root, "", ingester) }) return err } // lessPath returns true if one path a is less than path b. // // A component-wise comparison is done, rather than the lexical comparison of // strings. func lessPath(a, b string) bool { // we provide this behavior by making separator always sort first. return compareReplaceInline(a, b, '/', '\x00') < 0 } // compareReplaceInline modifies runtime.cmpstring to replace old with new // during a byte-wise comparison. func compareReplaceInline(s1, s2 string, old, new byte) int { // TODO(stevvooe): We are missing an optimization when the s1 and s2 have // the exact same slice header. It will make the code unsafe but can // provide some extra performance. l := len(s1) if len(s2) < l { l = len(s2) } for i := 0; i < l; i++ { c1, c2 := s1[i], s2[i] if c1 == old { c1 = new } if c2 == old { c2 = new } if c1 < c2 { return -1 } if c1 > c2 { return +1 } } if len(s1) < len(s2) { return -1 } if len(s1) > len(s2) { return +1 } return 0 } // handleRepository calls function fn with a repository path if fileInfo // has a path of a repository under root and that it is lexographically // after last. Otherwise, it will return ErrSkipDir. This should be used // with Walk to do handling with repositories in a storage. func handleRepository(fileInfo driver.FileInfo, root, last string, fn func(repoPath string) error) error { filePath := fileInfo.Path() // lop the base path off repo := filePath[len(root)+1:] _, file := path.Split(repo) if file == "_layers" { repo = strings.TrimSuffix(repo, "/_layers") if lessPath(last, repo) { if err := fn(repo); err != nil { return err } } return ErrSkipDir } else if strings.HasPrefix(file, "_") { return ErrSkipDir } return nil } docker-registry-2.6.2~ds1/registry/storage/catalog_test.go000066400000000000000000000155431313450123100237610ustar00rootroot00000000000000package storage import ( "fmt" "io" "math/rand" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" ) type setupEnv struct { ctx context.Context driver driver.StorageDriver expected []string registry distribution.Namespace } func setupFS(t *testing.T) *setupEnv { d := inmemory.New() ctx := context.Background() registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repos := []string{ "foo/a", "foo/b", "foo-bar/a", "bar/c", "bar/d", "bar/e", "foo/d/in", "foo-bar/b", "test", } for _, repo := range repos { makeRepo(ctx, t, repo, registry) } expected := []string{ "bar/c", "bar/d", "bar/e", "foo/a", "foo/b", "foo/d/in", "foo-bar/a", "foo-bar/b", "test", } return &setupEnv{ ctx: ctx, driver: d, expected: expected, registry: registry, } } func makeRepo(ctx context.Context, t *testing.T, name string, reg distribution.Namespace) { named, err := reference.ParseNamed(name) if err != nil { t.Fatal(err) } repo, _ := reg.Repository(ctx, named) manifests, _ := repo.Manifests(ctx) layers, err := testutil.CreateRandomLayers(1) if err != nil { t.Fatal(err) } err = testutil.UploadBlobs(repo, layers) if err != nil { t.Fatalf("failed to upload layers: %v", err) } getKeys := func(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) { for d := range digests { ds = append(ds, d) } return } manifest, err := testutil.MakeSchema1Manifest(getKeys(layers)) if err != nil { t.Fatal(err) } _, err = manifests.Put(ctx, manifest) if err != nil { t.Fatalf("manifest upload failed: %v", err) } } func TestCatalog(t *testing.T) { env := setupFS(t) p := make([]string, 50) numFilled, err := env.registry.Repositories(env.ctx, p, "") if numFilled != len(env.expected) { t.Errorf("missing items in catalog") } if !testEq(p, env.expected, len(env.expected)) { t.Errorf("Expected catalog repos err") } if err != io.EOF { t.Errorf("Catalog has more values which we aren't expecting") } } func TestCatalogInParts(t *testing.T) { env := setupFS(t) chunkLen := 3 p := make([]string, chunkLen) numFilled, err := env.registry.Repositories(env.ctx, p, "") if err == io.EOF || numFilled != len(p) { t.Errorf("Expected more values in catalog") } if !testEq(p, env.expected[0:chunkLen], numFilled) { t.Errorf("Expected catalog first chunk err") } lastRepo := p[len(p)-1] numFilled, err = env.registry.Repositories(env.ctx, p, lastRepo) if err == io.EOF || numFilled != len(p) { t.Errorf("Expected more values in catalog") } if !testEq(p, env.expected[chunkLen:chunkLen*2], numFilled) { t.Errorf("Expected catalog second chunk err") } lastRepo = p[len(p)-1] numFilled, err = env.registry.Repositories(env.ctx, p, lastRepo) if err != io.EOF || numFilled != len(p) { t.Errorf("Expected end of catalog") } if !testEq(p, env.expected[chunkLen*2:chunkLen*3], numFilled) { t.Errorf("Expected catalog third chunk err") } lastRepo = p[len(p)-1] numFilled, err = env.registry.Repositories(env.ctx, p, lastRepo) if err != io.EOF { t.Errorf("Catalog has more values which we aren't expecting") } if numFilled != 0 { t.Errorf("Expected catalog fourth chunk err") } } func TestCatalogEnumerate(t *testing.T) { env := setupFS(t) var repos []string repositoryEnumerator := env.registry.(distribution.RepositoryEnumerator) err := repositoryEnumerator.Enumerate(env.ctx, func(repoName string) error { repos = append(repos, repoName) return nil }) if err != nil { t.Errorf("Expected catalog enumerate err") } if len(repos) != len(env.expected) { t.Errorf("Expected catalog enumerate doesn't have correct number of values") } if !testEq(repos, env.expected, len(env.expected)) { t.Errorf("Expected catalog enumerate not over all values") } } func testEq(a, b []string, size int) bool { for cnt := 0; cnt < size-1; cnt++ { if a[cnt] != b[cnt] { return false } } return true } func setupBadWalkEnv(t *testing.T) *setupEnv { d := newBadListDriver() ctx := context.Background() registry, err := NewRegistry(ctx, d, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } return &setupEnv{ ctx: ctx, driver: d, registry: registry, } } type badListDriver struct { driver.StorageDriver } var _ driver.StorageDriver = &badListDriver{} func newBadListDriver() *badListDriver { return &badListDriver{StorageDriver: inmemory.New()} } func (d *badListDriver) List(ctx context.Context, path string) ([]string, error) { return nil, fmt.Errorf("List error") } func TestCatalogWalkError(t *testing.T) { env := setupBadWalkEnv(t) p := make([]string, 1) _, err := env.registry.Repositories(env.ctx, p, "") if err == io.EOF { t.Errorf("Expected catalog driver list error") } } func BenchmarkPathCompareEqual(B *testing.B) { B.StopTimer() pp := randomPath(100) // make a real copy ppb := append([]byte{}, []byte(pp)...) a, b := pp, string(ppb) B.StartTimer() for i := 0; i < B.N; i++ { lessPath(a, b) } } func BenchmarkPathCompareNotEqual(B *testing.B) { B.StopTimer() a, b := randomPath(100), randomPath(100) B.StartTimer() for i := 0; i < B.N; i++ { lessPath(a, b) } } func BenchmarkPathCompareNative(B *testing.B) { B.StopTimer() a, b := randomPath(100), randomPath(100) B.StartTimer() for i := 0; i < B.N; i++ { c := a < b c = c && false } } func BenchmarkPathCompareNativeEqual(B *testing.B) { B.StopTimer() pp := randomPath(100) a, b := pp, pp B.StartTimer() for i := 0; i < B.N; i++ { c := a < b c = c && false } } var filenameChars = []byte("abcdefghijklmnopqrstuvwxyz0123456789") var separatorChars = []byte("._-") func randomPath(length int64) string { path := "/" for int64(len(path)) < length { chunkLength := rand.Int63n(length-int64(len(path))) + 1 chunk := randomFilename(chunkLength) path += chunk remaining := length - int64(len(path)) if remaining == 1 { path += randomFilename(1) } else if remaining > 1 { path += "/" } } return path } func randomFilename(length int64) string { b := make([]byte, length) wasSeparator := true for i := range b { if !wasSeparator && i < len(b)-1 && rand.Intn(4) == 0 { b[i] = separatorChars[rand.Intn(len(separatorChars))] wasSeparator = true } else { b[i] = filenameChars[rand.Intn(len(filenameChars))] wasSeparator = false } } return string(b) } docker-registry-2.6.2~ds1/registry/storage/doc.go000066400000000000000000000002401313450123100220410ustar00rootroot00000000000000// Package storage contains storage services for use in the registry // application. It should be considered an internal package, as of Go 1.4. package storage docker-registry-2.6.2~ds1/registry/storage/driver/000077500000000000000000000000001313450123100222445ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/azure/000077500000000000000000000000001313450123100233725ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/azure/azure.go000066400000000000000000000301461313450123100250530ustar00rootroot00000000000000// Package azure provides a storagedriver.StorageDriver implementation to // store blobs in Microsoft Azure Blob Storage Service. package azure import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "net/http" "strings" "time" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" azure "github.com/Azure/azure-sdk-for-go/storage" ) const driverName = "azure" const ( paramAccountName = "accountname" paramAccountKey = "accountkey" paramContainer = "container" paramRealm = "realm" maxChunkSize = 4 * 1024 * 1024 ) type driver struct { client azure.BlobStorageClient container string } type baseEmbed struct{ base.Base } // Driver is a storagedriver.StorageDriver implementation backed by // Microsoft Azure Blob Storage Service. type Driver struct{ baseEmbed } func init() { factory.Register(driverName, &azureDriverFactory{}) } type azureDriverFactory struct{} func (factory *azureDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } // FromParameters constructs a new Driver with a given parameters map. func FromParameters(parameters map[string]interface{}) (*Driver, error) { accountName, ok := parameters[paramAccountName] if !ok || fmt.Sprint(accountName) == "" { return nil, fmt.Errorf("No %s parameter provided", paramAccountName) } accountKey, ok := parameters[paramAccountKey] if !ok || fmt.Sprint(accountKey) == "" { return nil, fmt.Errorf("No %s parameter provided", paramAccountKey) } container, ok := parameters[paramContainer] if !ok || fmt.Sprint(container) == "" { return nil, fmt.Errorf("No %s parameter provided", paramContainer) } realm, ok := parameters[paramRealm] if !ok || fmt.Sprint(realm) == "" { realm = azure.DefaultBaseURL } return New(fmt.Sprint(accountName), fmt.Sprint(accountKey), fmt.Sprint(container), fmt.Sprint(realm)) } // New constructs a new Driver with the given Azure Storage Account credentials func New(accountName, accountKey, container, realm string) (*Driver, error) { api, err := azure.NewClient(accountName, accountKey, realm, azure.DefaultAPIVersion, true) if err != nil { return nil, err } blobClient := api.GetBlobService() // Create registry container if _, err = blobClient.CreateContainerIfNotExists(container, azure.ContainerAccessTypePrivate); err != nil { return nil, err } d := &driver{ client: blobClient, container: container} return &Driver{baseEmbed: baseEmbed{Base: base.Base{StorageDriver: d}}}, nil } // Implement the storagedriver.StorageDriver interface. func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { blob, err := d.client.GetBlob(d.container, path) if err != nil { if is404(err) { return nil, storagedriver.PathNotFoundError{Path: path} } return nil, err } defer blob.Close() return ioutil.ReadAll(blob) } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { if _, err := d.client.DeleteBlobIfExists(d.container, path, nil); err != nil { return err } writer, err := d.Writer(ctx, path, false) if err != nil { return err } defer writer.Close() _, err = writer.Write(contents) if err != nil { return err } return writer.Commit() } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { if ok, err := d.client.BlobExists(d.container, path); err != nil { return nil, err } else if !ok { return nil, storagedriver.PathNotFoundError{Path: path} } info, err := d.client.GetBlobProperties(d.container, path) if err != nil { return nil, err } size := int64(info.ContentLength) if offset >= size { return ioutil.NopCloser(bytes.NewReader(nil)), nil } bytesRange := fmt.Sprintf("%v-", offset) resp, err := d.client.GetBlobRange(d.container, path, bytesRange, nil) if err != nil { return nil, err } return resp, nil } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { blobExists, err := d.client.BlobExists(d.container, path) if err != nil { return nil, err } var size int64 if blobExists { if append { blobProperties, err := d.client.GetBlobProperties(d.container, path) if err != nil { return nil, err } size = blobProperties.ContentLength } else { err := d.client.DeleteBlob(d.container, path, nil) if err != nil { return nil, err } } } else { if append { return nil, storagedriver.PathNotFoundError{Path: path} } err := d.client.PutAppendBlob(d.container, path, nil) if err != nil { return nil, err } } return d.newWriter(path, size), nil } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { // Check if the path is a blob if ok, err := d.client.BlobExists(d.container, path); err != nil { return nil, err } else if ok { blob, err := d.client.GetBlobProperties(d.container, path) if err != nil { return nil, err } mtim, err := time.Parse(http.TimeFormat, blob.LastModified) if err != nil { return nil, err } return storagedriver.FileInfoInternal{FileInfoFields: storagedriver.FileInfoFields{ Path: path, Size: int64(blob.ContentLength), ModTime: mtim, IsDir: false, }}, nil } // Check if path is a virtual container virtContainerPath := path if !strings.HasSuffix(virtContainerPath, "/") { virtContainerPath += "/" } blobs, err := d.client.ListBlobs(d.container, azure.ListBlobsParameters{ Prefix: virtContainerPath, MaxResults: 1, }) if err != nil { return nil, err } if len(blobs.Blobs) > 0 { // path is a virtual container return storagedriver.FileInfoInternal{FileInfoFields: storagedriver.FileInfoFields{ Path: path, IsDir: true, }}, nil } // path is not a blob or virtual container return nil, storagedriver.PathNotFoundError{Path: path} } // List returns a list of the objects that are direct descendants of the given // path. func (d *driver) List(ctx context.Context, path string) ([]string, error) { if path == "/" { path = "" } blobs, err := d.listBlobs(d.container, path) if err != nil { return blobs, err } list := directDescendants(blobs, path) if path != "" && len(list) == 0 { return nil, storagedriver.PathNotFoundError{Path: path} } return list, nil } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { sourceBlobURL := d.client.GetBlobURL(d.container, sourcePath) err := d.client.CopyBlob(d.container, destPath, sourceBlobURL) if err != nil { if is404(err) { return storagedriver.PathNotFoundError{Path: sourcePath} } return err } return d.client.DeleteBlob(d.container, sourcePath, nil) } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, path string) error { ok, err := d.client.DeleteBlobIfExists(d.container, path, nil) if err != nil { return err } if ok { return nil // was a blob and deleted, return } // Not a blob, see if path is a virtual container with blobs blobs, err := d.listBlobs(d.container, path) if err != nil { return err } for _, b := range blobs { if err = d.client.DeleteBlob(d.container, b, nil); err != nil { return err } } if len(blobs) == 0 { return storagedriver.PathNotFoundError{Path: path} } return nil } // URLFor returns a publicly accessible URL for the blob stored at given path // for specified duration by making use of Azure Storage Shared Access Signatures (SAS). // See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx for more info. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { expiresTime := time.Now().UTC().Add(20 * time.Minute) // default expiration expires, ok := options["expiry"] if ok { t, ok := expires.(time.Time) if ok { expiresTime = t } } return d.client.GetBlobSASURI(d.container, path, expiresTime, "r") } // directDescendants will find direct descendants (blobs or virtual containers) // of from list of blob paths and will return their full paths. Elements in blobs // list must be prefixed with a "/" and // // Example: direct descendants of "/" in {"/foo", "/bar/1", "/bar/2"} is // {"/foo", "/bar"} and direct descendants of "bar" is {"/bar/1", "/bar/2"} func directDescendants(blobs []string, prefix string) []string { if !strings.HasPrefix(prefix, "/") { // add trailing '/' prefix = "/" + prefix } if !strings.HasSuffix(prefix, "/") { // containerify the path prefix += "/" } out := make(map[string]bool) for _, b := range blobs { if strings.HasPrefix(b, prefix) { rel := b[len(prefix):] c := strings.Count(rel, "/") if c == 0 { out[b] = true } else { out[prefix+rel[:strings.Index(rel, "/")]] = true } } } var keys []string for k := range out { keys = append(keys, k) } return keys } func (d *driver) listBlobs(container, virtPath string) ([]string, error) { if virtPath != "" && !strings.HasSuffix(virtPath, "/") { // containerify the path virtPath += "/" } out := []string{} marker := "" for { resp, err := d.client.ListBlobs(d.container, azure.ListBlobsParameters{ Marker: marker, Prefix: virtPath, }) if err != nil { return out, err } for _, b := range resp.Blobs { out = append(out, b.Name) } if len(resp.Blobs) == 0 || resp.NextMarker == "" { break } marker = resp.NextMarker } return out, nil } func is404(err error) bool { statusCodeErr, ok := err.(azure.AzureStorageServiceError) return ok && statusCodeErr.StatusCode == http.StatusNotFound } type writer struct { driver *driver path string size int64 bw *bufio.Writer closed bool committed bool cancelled bool } func (d *driver) newWriter(path string, size int64) storagedriver.FileWriter { return &writer{ driver: d, path: path, size: size, bw: bufio.NewWriterSize(&blockWriter{ client: d.client, container: d.container, path: path, }, maxChunkSize), } } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } n, err := w.bw.Write(p) w.size += int64(n) return n, err } func (w *writer) Size() int64 { return w.size } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } w.closed = true return w.bw.Flush() } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true return w.driver.client.DeleteBlob(w.driver.container, w.path, nil) } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } w.committed = true return w.bw.Flush() } type blockWriter struct { client azure.BlobStorageClient container string path string } func (bw *blockWriter) Write(p []byte) (int, error) { n := 0 for offset := 0; offset < len(p); offset += maxChunkSize { chunkSize := maxChunkSize if offset+chunkSize > len(p) { chunkSize = len(p) - offset } err := bw.client.AppendBlock(bw.container, bw.path, p[offset:offset+chunkSize], nil) if err != nil { return n, err } n += chunkSize } return n, nil } docker-registry-2.6.2~ds1/registry/storage/driver/azure/azure_test.go000066400000000000000000000025721313450123100261140ustar00rootroot00000000000000package azure import ( "fmt" "os" "strings" "testing" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" . "gopkg.in/check.v1" ) const ( envAccountName = "AZURE_STORAGE_ACCOUNT_NAME" envAccountKey = "AZURE_STORAGE_ACCOUNT_KEY" envContainer = "AZURE_STORAGE_CONTAINER" envRealm = "AZURE_STORAGE_REALM" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { TestingT(t) } func init() { var ( accountName string accountKey string container string realm string ) config := []struct { env string value *string }{ {envAccountName, &accountName}, {envAccountKey, &accountKey}, {envContainer, &container}, {envRealm, &realm}, } missing := []string{} for _, v := range config { *v.value = os.Getenv(v.env) if *v.value == "" { missing = append(missing, v.env) } } azureDriverConstructor := func() (storagedriver.StorageDriver, error) { return New(accountName, accountKey, container, realm) } // Skip Azure storage driver tests if environment variable parameters are not provided skipCheck := func() string { if len(missing) > 0 { return fmt.Sprintf("Must set %s environment variables to run Azure tests", strings.Join(missing, ", ")) } return "" } testsuites.RegisterSuite(azureDriverConstructor, skipCheck) } docker-registry-2.6.2~ds1/registry/storage/driver/base/000077500000000000000000000000001313450123100231565ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/base/base.go000066400000000000000000000153051313450123100244230ustar00rootroot00000000000000// Package base provides a base implementation of the storage driver that can // be used to implement common checks. The goal is to increase the amount of // code sharing. // // The canonical approach to use this class is to embed in the exported driver // struct such that calls are proxied through this implementation. First, // declare the internal driver, as follows: // // type driver struct { ... internal ...} // // The resulting type should implement StorageDriver such that it can be the // target of a Base struct. The exported type can then be declared as follows: // // type Driver struct { // Base // } // // Because Driver embeds Base, it effectively implements Base. If the driver // needs to intercept a call, before going to base, Driver should implement // that method. Effectively, Driver can intercept calls before coming in and // driver implements the actual logic. // // To further shield the embed from other packages, it is recommended to // employ a private embed struct: // // type baseEmbed struct { // base.Base // } // // Then, declare driver to embed baseEmbed, rather than Base directly: // // type Driver struct { // baseEmbed // } // // The type now implements StorageDriver, proxying through Base, without // exporting an unnecessary field. package base import ( "io" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" ) // Base provides a wrapper around a storagedriver implementation that provides // common path and bounds checking. type Base struct { storagedriver.StorageDriver } // Format errors received from the storage driver func (base *Base) setDriverName(e error) error { switch actual := e.(type) { case nil: return nil case storagedriver.ErrUnsupportedMethod: actual.DriverName = base.StorageDriver.Name() return actual case storagedriver.PathNotFoundError: actual.DriverName = base.StorageDriver.Name() return actual case storagedriver.InvalidPathError: actual.DriverName = base.StorageDriver.Name() return actual case storagedriver.InvalidOffsetError: actual.DriverName = base.StorageDriver.Name() return actual default: storageError := storagedriver.Error{ DriverName: base.StorageDriver.Name(), Enclosed: e, } return storageError } } // GetContent wraps GetContent of underlying storage driver. func (base *Base) GetContent(ctx context.Context, path string) ([]byte, error) { ctx, done := context.WithTrace(ctx) defer done("%s.GetContent(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) { return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } b, e := base.StorageDriver.GetContent(ctx, path) return b, base.setDriverName(e) } // PutContent wraps PutContent of underlying storage driver. func (base *Base) PutContent(ctx context.Context, path string, content []byte) error { ctx, done := context.WithTrace(ctx) defer done("%s.PutContent(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) { return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } return base.setDriverName(base.StorageDriver.PutContent(ctx, path, content)) } // Reader wraps Reader of underlying storage driver. func (base *Base) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { ctx, done := context.WithTrace(ctx) defer done("%s.Reader(%q, %d)", base.Name(), path, offset) if offset < 0 { return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset, DriverName: base.StorageDriver.Name()} } if !storagedriver.PathRegexp.MatchString(path) { return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } rc, e := base.StorageDriver.Reader(ctx, path, offset) return rc, base.setDriverName(e) } // Writer wraps Writer of underlying storage driver. func (base *Base) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { ctx, done := context.WithTrace(ctx) defer done("%s.Writer(%q, %v)", base.Name(), path, append) if !storagedriver.PathRegexp.MatchString(path) { return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } writer, e := base.StorageDriver.Writer(ctx, path, append) return writer, base.setDriverName(e) } // Stat wraps Stat of underlying storage driver. func (base *Base) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { ctx, done := context.WithTrace(ctx) defer done("%s.Stat(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) && path != "/" { return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } fi, e := base.StorageDriver.Stat(ctx, path) return fi, base.setDriverName(e) } // List wraps List of underlying storage driver. func (base *Base) List(ctx context.Context, path string) ([]string, error) { ctx, done := context.WithTrace(ctx) defer done("%s.List(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) && path != "/" { return nil, storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } str, e := base.StorageDriver.List(ctx, path) return str, base.setDriverName(e) } // Move wraps Move of underlying storage driver. func (base *Base) Move(ctx context.Context, sourcePath string, destPath string) error { ctx, done := context.WithTrace(ctx) defer done("%s.Move(%q, %q", base.Name(), sourcePath, destPath) if !storagedriver.PathRegexp.MatchString(sourcePath) { return storagedriver.InvalidPathError{Path: sourcePath, DriverName: base.StorageDriver.Name()} } else if !storagedriver.PathRegexp.MatchString(destPath) { return storagedriver.InvalidPathError{Path: destPath, DriverName: base.StorageDriver.Name()} } return base.setDriverName(base.StorageDriver.Move(ctx, sourcePath, destPath)) } // Delete wraps Delete of underlying storage driver. func (base *Base) Delete(ctx context.Context, path string) error { ctx, done := context.WithTrace(ctx) defer done("%s.Delete(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) { return storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } return base.setDriverName(base.StorageDriver.Delete(ctx, path)) } // URLFor wraps URLFor of underlying storage driver. func (base *Base) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { ctx, done := context.WithTrace(ctx) defer done("%s.URLFor(%q)", base.Name(), path) if !storagedriver.PathRegexp.MatchString(path) { return "", storagedriver.InvalidPathError{Path: path, DriverName: base.StorageDriver.Name()} } str, e := base.StorageDriver.URLFor(ctx, path, options) return str, base.setDriverName(e) } docker-registry-2.6.2~ds1/registry/storage/driver/base/regulator.go000066400000000000000000000102361313450123100255130ustar00rootroot00000000000000package base import ( "io" "sync" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" ) type regulator struct { storagedriver.StorageDriver *sync.Cond available uint64 } // NewRegulator wraps the given driver and is used to regulate concurrent calls // to the given storage driver to a maximum of the given limit. This is useful // for storage drivers that would otherwise create an unbounded number of OS // threads if allowed to be called unregulated. func NewRegulator(driver storagedriver.StorageDriver, limit uint64) storagedriver.StorageDriver { return ®ulator{ StorageDriver: driver, Cond: sync.NewCond(&sync.Mutex{}), available: limit, } } func (r *regulator) enter() { r.L.Lock() for r.available == 0 { r.Wait() } r.available-- r.L.Unlock() } func (r *regulator) exit() { r.L.Lock() // We only need to signal to a waiting FS operation if we're already at the // limit of threads used if r.available == 0 { r.Signal() } r.available++ r.L.Unlock() } // Name returns the human-readable "name" of the driver, useful in error // messages and logging. By convention, this will just be the registration // name, but drivers may provide other information here. func (r *regulator) Name() string { r.enter() defer r.exit() return r.StorageDriver.Name() } // GetContent retrieves the content stored at "path" as a []byte. // This should primarily be used for small objects. func (r *regulator) GetContent(ctx context.Context, path string) ([]byte, error) { r.enter() defer r.exit() return r.StorageDriver.GetContent(ctx, path) } // PutContent stores the []byte content at a location designated by "path". // This should primarily be used for small objects. func (r *regulator) PutContent(ctx context.Context, path string, content []byte) error { r.enter() defer r.exit() return r.StorageDriver.PutContent(ctx, path, content) } // Reader retrieves an io.ReadCloser for the content stored at "path" // with a given byte offset. // May be used to resume reading a stream by providing a nonzero offset. func (r *regulator) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { r.enter() defer r.exit() return r.StorageDriver.Reader(ctx, path, offset) } // Writer stores the contents of the provided io.ReadCloser at a // location designated by the given path. // May be used to resume writing a stream by providing a nonzero offset. // The offset must be no larger than the CurrentSize for this path. func (r *regulator) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { r.enter() defer r.exit() return r.StorageDriver.Writer(ctx, path, append) } // Stat retrieves the FileInfo for the given path, including the current // size in bytes and the creation time. func (r *regulator) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { r.enter() defer r.exit() return r.StorageDriver.Stat(ctx, path) } // List returns a list of the objects that are direct descendants of the //given path. func (r *regulator) List(ctx context.Context, path string) ([]string, error) { r.enter() defer r.exit() return r.StorageDriver.List(ctx, path) } // Move moves an object stored at sourcePath to destPath, removing the // original object. // Note: This may be no more efficient than a copy followed by a delete for // many implementations. func (r *regulator) Move(ctx context.Context, sourcePath string, destPath string) error { r.enter() defer r.exit() return r.StorageDriver.Move(ctx, sourcePath, destPath) } // Delete recursively deletes all objects stored at "path" and its subpaths. func (r *regulator) Delete(ctx context.Context, path string) error { r.enter() defer r.exit() return r.StorageDriver.Delete(ctx, path) } // URLFor returns a URL which may be used to retrieve the content stored at // the given path, possibly using the given options. // May return an ErrUnsupportedMethod in certain StorageDriver // implementations. func (r *regulator) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { r.enter() defer r.exit() return r.StorageDriver.URLFor(ctx, path, options) } docker-registry-2.6.2~ds1/registry/storage/driver/factory/000077500000000000000000000000001313450123100237135ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/factory/factory.go000066400000000000000000000052351313450123100257160ustar00rootroot00000000000000package factory import ( "fmt" storagedriver "github.com/docker/distribution/registry/storage/driver" ) // driverFactories stores an internal mapping between storage driver names and their respective // factories var driverFactories = make(map[string]StorageDriverFactory) // StorageDriverFactory is a factory interface for creating storagedriver.StorageDriver interfaces // Storage drivers should call Register() with a factory to make the driver available by name. // Individual StorageDriver implementations generally register with the factory via the Register // func (below) in their init() funcs, and as such they should be imported anonymously before use. // See below for an example of how to register and get a StorageDriver for S3 // // import _ "github.com/docker/distribution/registry/storage/driver/s3-aws" // s3Driver, err = factory.Create("s3", storageParams) // // assuming no error, s3Driver is the StorageDriver that communicates with S3 according to storageParams type StorageDriverFactory interface { // Create returns a new storagedriver.StorageDriver with the given parameters // Parameters will vary by driver and may be ignored // Each parameter key must only consist of lowercase letters and numbers Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) } // Register makes a storage driver available by the provided name. // If Register is called twice with the same name or if driver factory is nil, it panics. // Additionally, it is not concurrency safe. Most Storage Drivers call this function // in their init() functions. See the documentation for StorageDriverFactory for more. func Register(name string, factory StorageDriverFactory) { if factory == nil { panic("Must not provide nil StorageDriverFactory") } _, registered := driverFactories[name] if registered { panic(fmt.Sprintf("StorageDriverFactory named %s already registered", name)) } driverFactories[name] = factory } // Create a new storagedriver.StorageDriver with the given name and // parameters. To use a driver, the StorageDriverFactory must first be // registered with the given name. If no drivers are found, an // InvalidStorageDriverError is returned func Create(name string, parameters map[string]interface{}) (storagedriver.StorageDriver, error) { driverFactory, ok := driverFactories[name] if !ok { return nil, InvalidStorageDriverError{name} } return driverFactory.Create(parameters) } // InvalidStorageDriverError records an attempt to construct an unregistered storage driver type InvalidStorageDriverError struct { Name string } func (err InvalidStorageDriverError) Error() string { return fmt.Sprintf("StorageDriver not registered: %s", err.Name) } docker-registry-2.6.2~ds1/registry/storage/driver/fileinfo.go000066400000000000000000000051341313450123100243710ustar00rootroot00000000000000package driver import "time" // FileInfo returns information about a given path. Inspired by os.FileInfo, // it elides the base name method for a full path instead. type FileInfo interface { // Path provides the full path of the target of this file info. Path() string // Size returns current length in bytes of the file. The return value can // be used to write to the end of the file at path. The value is // meaningless if IsDir returns true. Size() int64 // ModTime returns the modification time for the file. For backends that // don't have a modification time, the creation time should be returned. ModTime() time.Time // IsDir returns true if the path is a directory. IsDir() bool } // NOTE(stevvooe): The next two types, FileInfoFields and FileInfoInternal // should only be used by storagedriver implementations. They should moved to // a "driver" package, similar to database/sql. // FileInfoFields provides the exported fields for implementing FileInfo // interface in storagedriver implementations. It should be used with // InternalFileInfo. type FileInfoFields struct { // Path provides the full path of the target of this file info. Path string // Size is current length in bytes of the file. The value of this field // can be used to write to the end of the file at path. The value is // meaningless if IsDir is set to true. Size int64 // ModTime returns the modification time for the file. For backends that // don't have a modification time, the creation time should be returned. ModTime time.Time // IsDir returns true if the path is a directory. IsDir bool } // FileInfoInternal implements the FileInfo interface. This should only be // used by storagedriver implementations that don't have a specialized // FileInfo type. type FileInfoInternal struct { FileInfoFields } var _ FileInfo = FileInfoInternal{} var _ FileInfo = &FileInfoInternal{} // Path provides the full path of the target of this file info. func (fi FileInfoInternal) Path() string { return fi.FileInfoFields.Path } // Size returns current length in bytes of the file. The return value can // be used to write to the end of the file at path. The value is // meaningless if IsDir returns true. func (fi FileInfoInternal) Size() int64 { return fi.FileInfoFields.Size } // ModTime returns the modification time for the file. For backends that // don't have a modification time, the creation time should be returned. func (fi FileInfoInternal) ModTime() time.Time { return fi.FileInfoFields.ModTime } // IsDir returns true if the path is a directory. func (fi FileInfoInternal) IsDir() bool { return fi.FileInfoFields.IsDir } docker-registry-2.6.2~ds1/registry/storage/driver/filesystem/000077500000000000000000000000001313450123100244305ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/filesystem/driver.go000066400000000000000000000242331313450123100262560ustar00rootroot00000000000000package filesystem import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "os" "path" "reflect" "strconv" "time" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const ( driverName = "filesystem" defaultRootDirectory = "/var/lib/registry" defaultMaxThreads = uint64(100) // minThreads is the minimum value for the maxthreads configuration // parameter. If the driver's parameters are less than this we set // the parameters to minThreads minThreads = uint64(25) ) // DriverParameters represents all configuration options available for the // filesystem driver type DriverParameters struct { RootDirectory string MaxThreads uint64 } func init() { factory.Register(driverName, &filesystemDriverFactory{}) } // filesystemDriverFactory implements the factory.StorageDriverFactory interface type filesystemDriverFactory struct{} func (factory *filesystemDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } type driver struct { rootDirectory string } type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by a local // filesystem. All provided paths will be subpaths of the RootDirectory. type Driver struct { baseEmbed } // FromParameters constructs a new Driver with a given parameters map // Optional Parameters: // - rootdirectory // - maxthreads func FromParameters(parameters map[string]interface{}) (*Driver, error) { params, err := fromParametersImpl(parameters) if err != nil || params == nil { return nil, err } return New(*params), nil } func fromParametersImpl(parameters map[string]interface{}) (*DriverParameters, error) { var ( err error maxThreads = defaultMaxThreads rootDirectory = defaultRootDirectory ) if parameters != nil { if rootDir, ok := parameters["rootdirectory"]; ok { rootDirectory = fmt.Sprint(rootDir) } // Get maximum number of threads for blocking filesystem operations, // if specified threads := parameters["maxthreads"] switch v := threads.(type) { case string: if maxThreads, err = strconv.ParseUint(v, 0, 64); err != nil { return nil, fmt.Errorf("maxthreads parameter must be an integer, %v invalid", threads) } case uint64: maxThreads = v case int, int32, int64: val := reflect.ValueOf(v).Convert(reflect.TypeOf(threads)).Int() // If threads is negative casting to uint64 will wrap around and // give you the hugest thread limit ever. Let's be sensible, here if val > 0 { maxThreads = uint64(val) } case uint, uint32: maxThreads = reflect.ValueOf(v).Convert(reflect.TypeOf(threads)).Uint() case nil: // do nothing default: return nil, fmt.Errorf("invalid value for maxthreads: %#v", threads) } if maxThreads < minThreads { maxThreads = minThreads } } params := &DriverParameters{ RootDirectory: rootDirectory, MaxThreads: maxThreads, } return params, nil } // New constructs a new Driver with a given rootDirectory func New(params DriverParameters) *Driver { fsDriver := &driver{rootDirectory: params.RootDirectory} return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: base.NewRegulator(fsDriver, params.MaxThreads), }, }, } } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { rc, err := d.Reader(ctx, path, 0) if err != nil { return nil, err } defer rc.Close() p, err := ioutil.ReadAll(rc) if err != nil { return nil, err } return p, nil } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, subPath string, contents []byte) error { writer, err := d.Writer(ctx, subPath, false) if err != nil { return err } defer writer.Close() _, err = io.Copy(writer, bytes.NewReader(contents)) if err != nil { writer.Cancel() return err } return writer.Commit() } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { file, err := os.OpenFile(d.fullPath(path), os.O_RDONLY, 0644) if err != nil { if os.IsNotExist(err) { return nil, storagedriver.PathNotFoundError{Path: path} } return nil, err } seekPos, err := file.Seek(int64(offset), os.SEEK_SET) if err != nil { file.Close() return nil, err } else if seekPos < int64(offset) { file.Close() return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset} } return file, nil } func (d *driver) Writer(ctx context.Context, subPath string, append bool) (storagedriver.FileWriter, error) { fullPath := d.fullPath(subPath) parentDir := path.Dir(fullPath) if err := os.MkdirAll(parentDir, 0777); err != nil { return nil, err } fp, err := os.OpenFile(fullPath, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { return nil, err } var offset int64 if !append { err := fp.Truncate(0) if err != nil { fp.Close() return nil, err } } else { n, err := fp.Seek(0, os.SEEK_END) if err != nil { fp.Close() return nil, err } offset = int64(n) } return newFileWriter(fp, offset), nil } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, subPath string) (storagedriver.FileInfo, error) { fullPath := d.fullPath(subPath) fi, err := os.Stat(fullPath) if err != nil { if os.IsNotExist(err) { return nil, storagedriver.PathNotFoundError{Path: subPath} } return nil, err } return fileInfo{ path: subPath, FileInfo: fi, }, nil } // List returns a list of the objects that are direct descendants of the given // path. func (d *driver) List(ctx context.Context, subPath string) ([]string, error) { fullPath := d.fullPath(subPath) dir, err := os.Open(fullPath) if err != nil { if os.IsNotExist(err) { return nil, storagedriver.PathNotFoundError{Path: subPath} } return nil, err } defer dir.Close() fileNames, err := dir.Readdirnames(0) if err != nil { return nil, err } keys := make([]string, 0, len(fileNames)) for _, fileName := range fileNames { keys = append(keys, path.Join(subPath, fileName)) } return keys, nil } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { source := d.fullPath(sourcePath) dest := d.fullPath(destPath) if _, err := os.Stat(source); os.IsNotExist(err) { return storagedriver.PathNotFoundError{Path: sourcePath} } if err := os.MkdirAll(path.Dir(dest), 0755); err != nil { return err } err := os.Rename(source, dest) return err } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, subPath string) error { fullPath := d.fullPath(subPath) _, err := os.Stat(fullPath) if err != nil && !os.IsNotExist(err) { return err } else if err != nil { return storagedriver.PathNotFoundError{Path: subPath} } err = os.RemoveAll(fullPath) return err } // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { return "", storagedriver.ErrUnsupportedMethod{} } // fullPath returns the absolute path of a key within the Driver's storage. func (d *driver) fullPath(subPath string) string { return path.Join(d.rootDirectory, subPath) } type fileInfo struct { os.FileInfo path string } var _ storagedriver.FileInfo = fileInfo{} // Path provides the full path of the target of this file info. func (fi fileInfo) Path() string { return fi.path } // Size returns current length in bytes of the file. The return value can // be used to write to the end of the file at path. The value is // meaningless if IsDir returns true. func (fi fileInfo) Size() int64 { if fi.IsDir() { return 0 } return fi.FileInfo.Size() } // ModTime returns the modification time for the file. For backends that // don't have a modification time, the creation time should be returned. func (fi fileInfo) ModTime() time.Time { return fi.FileInfo.ModTime() } // IsDir returns true if the path is a directory. func (fi fileInfo) IsDir() bool { return fi.FileInfo.IsDir() } type fileWriter struct { file *os.File size int64 bw *bufio.Writer closed bool committed bool cancelled bool } func newFileWriter(file *os.File, size int64) *fileWriter { return &fileWriter{ file: file, size: size, bw: bufio.NewWriter(file), } } func (fw *fileWriter) Write(p []byte) (int, error) { if fw.closed { return 0, fmt.Errorf("already closed") } else if fw.committed { return 0, fmt.Errorf("already committed") } else if fw.cancelled { return 0, fmt.Errorf("already cancelled") } n, err := fw.bw.Write(p) fw.size += int64(n) return n, err } func (fw *fileWriter) Size() int64 { return fw.size } func (fw *fileWriter) Close() error { if fw.closed { return fmt.Errorf("already closed") } if err := fw.bw.Flush(); err != nil { return err } if err := fw.file.Sync(); err != nil { return err } if err := fw.file.Close(); err != nil { return err } fw.closed = true return nil } func (fw *fileWriter) Cancel() error { if fw.closed { return fmt.Errorf("already closed") } fw.cancelled = true fw.file.Close() return os.Remove(fw.file.Name()) } func (fw *fileWriter) Commit() error { if fw.closed { return fmt.Errorf("already closed") } else if fw.committed { return fmt.Errorf("already committed") } else if fw.cancelled { return fmt.Errorf("already cancelled") } if err := fw.bw.Flush(); err != nil { return err } if err := fw.file.Sync(); err != nil { return err } fw.committed = true return nil } docker-registry-2.6.2~ds1/registry/storage/driver/filesystem/driver_test.go000066400000000000000000000047561313450123100273250ustar00rootroot00000000000000package filesystem import ( "io/ioutil" "os" "reflect" "testing" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" . "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { TestingT(t) } func init() { root, err := ioutil.TempDir("", "driver-") if err != nil { panic(err) } defer os.Remove(root) driver, err := FromParameters(map[string]interface{}{ "rootdirectory": root, }) if err != nil { panic(err) } testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) { return driver, nil }, testsuites.NeverSkip) } func TestFromParametersImpl(t *testing.T) { tests := []struct { params map[string]interface{} // techincally the yaml can contain anything expected DriverParameters pass bool }{ // check we use default threads and root dirs { params: map[string]interface{}{}, expected: DriverParameters{ RootDirectory: defaultRootDirectory, MaxThreads: defaultMaxThreads, }, pass: true, }, // Testing initiation with a string maxThreads which can't be parsed { params: map[string]interface{}{ "maxthreads": "fail", }, expected: DriverParameters{}, pass: false, }, { params: map[string]interface{}{ "maxthreads": "100", }, expected: DriverParameters{ RootDirectory: defaultRootDirectory, MaxThreads: uint64(100), }, pass: true, }, { params: map[string]interface{}{ "maxthreads": 100, }, expected: DriverParameters{ RootDirectory: defaultRootDirectory, MaxThreads: uint64(100), }, pass: true, }, // check that we use minimum thread counts { params: map[string]interface{}{ "maxthreads": 1, }, expected: DriverParameters{ RootDirectory: defaultRootDirectory, MaxThreads: minThreads, }, pass: true, }, } for _, item := range tests { params, err := fromParametersImpl(item.params) if !item.pass { // We only need to assert that expected failures have an error if err == nil { t.Fatalf("expected error configuring filesystem driver with invalid param: %+v", item.params) } continue } if err != nil { t.Fatalf("unexpected error creating filesystem driver: %s", err) } // Note that we get a pointer to params back if !reflect.DeepEqual(*params, item.expected) { t.Fatalf("unexpected params from filesystem driver. expected %+v, got %+v", item.expected, params) } } } docker-registry-2.6.2~ds1/registry/storage/driver/gcs/000077500000000000000000000000001313450123100230205ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/gcs/doc.go000066400000000000000000000002231313450123100241110ustar00rootroot00000000000000// Package gcs implements the Google Cloud Storage driver backend. Support can be // enabled by including the "include_gcs" build tag. package gcs docker-registry-2.6.2~ds1/registry/storage/driver/gcs/gcs.go000066400000000000000000000563101313450123100241300ustar00rootroot00000000000000// Package gcs provides a storagedriver.StorageDriver implementation to // store blobs in Google cloud storage. // // This package leverages the google.golang.org/cloud/storage client library //for interfacing with gcs. // // Because gcs is a key, value store the Stat call does not support last modification // time for directories (directories are an abstraction for key, value stores) // // Note that the contents of incomplete uploads are not accessible even though // Stat returns their length // // +build include_gcs package gcs import ( "bytes" "fmt" "io" "io/ioutil" "math/rand" "net/http" "net/url" "reflect" "regexp" "sort" "strconv" "strings" "time" "golang.org/x/net/context" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "golang.org/x/oauth2/jwt" "google.golang.org/api/googleapi" "google.golang.org/cloud" "google.golang.org/cloud/storage" "github.com/Sirupsen/logrus" ctx "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const ( driverName = "gcs" dummyProjectID = "" uploadSessionContentType = "application/x-docker-upload-session" minChunkSize = 256 * 1024 defaultChunkSize = 20 * minChunkSize maxTries = 5 ) var rangeHeader = regexp.MustCompile(`^bytes=([0-9])+-([0-9]+)$`) // driverParameters is a struct that encapsulates all of the driver parameters after all values have been set type driverParameters struct { bucket string config *jwt.Config email string privateKey []byte client *http.Client rootDirectory string chunkSize int } func init() { factory.Register(driverName, &gcsDriverFactory{}) } // gcsDriverFactory implements the factory.StorageDriverFactory interface type gcsDriverFactory struct{} // Create StorageDriver from parameters func (factory *gcsDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } // driver is a storagedriver.StorageDriver implementation backed by GCS // Objects are stored at absolute keys in the provided bucket. type driver struct { client *http.Client bucket string email string privateKey []byte rootDirectory string chunkSize int } // FromParameters constructs a new Driver with a given parameters map // Required parameters: // - bucket func FromParameters(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { bucket, ok := parameters["bucket"] if !ok || fmt.Sprint(bucket) == "" { return nil, fmt.Errorf("No bucket parameter provided") } rootDirectory, ok := parameters["rootdirectory"] if !ok { rootDirectory = "" } chunkSize := defaultChunkSize chunkSizeParam, ok := parameters["chunksize"] if ok { switch v := chunkSizeParam.(type) { case string: vv, err := strconv.Atoi(v) if err != nil { return nil, fmt.Errorf("chunksize parameter must be an integer, %v invalid", chunkSizeParam) } chunkSize = vv case int, uint, int32, uint32, uint64, int64: chunkSize = int(reflect.ValueOf(v).Convert(reflect.TypeOf(chunkSize)).Int()) default: return nil, fmt.Errorf("invalid valud for chunksize: %#v", chunkSizeParam) } if chunkSize < minChunkSize { return nil, fmt.Errorf("The chunksize %#v parameter should be a number that is larger than or equal to %d", chunkSize, minChunkSize) } if chunkSize%minChunkSize != 0 { return nil, fmt.Errorf("chunksize should be a multiple of %d", minChunkSize) } } var ts oauth2.TokenSource jwtConf := new(jwt.Config) if keyfile, ok := parameters["keyfile"]; ok { jsonKey, err := ioutil.ReadFile(fmt.Sprint(keyfile)) if err != nil { return nil, err } jwtConf, err = google.JWTConfigFromJSON(jsonKey, storage.ScopeFullControl) if err != nil { return nil, err } ts = jwtConf.TokenSource(context.Background()) } else { var err error ts, err = google.DefaultTokenSource(context.Background(), storage.ScopeFullControl) if err != nil { return nil, err } } params := driverParameters{ bucket: fmt.Sprint(bucket), rootDirectory: fmt.Sprint(rootDirectory), email: jwtConf.Email, privateKey: jwtConf.PrivateKey, client: oauth2.NewClient(context.Background(), ts), chunkSize: chunkSize, } return New(params) } // New constructs a new driver func New(params driverParameters) (storagedriver.StorageDriver, error) { rootDirectory := strings.Trim(params.rootDirectory, "/") if rootDirectory != "" { rootDirectory += "/" } if params.chunkSize <= 0 || params.chunkSize%minChunkSize != 0 { return nil, fmt.Errorf("Invalid chunksize: %d is not a positive multiple of %d", params.chunkSize, minChunkSize) } d := &driver{ bucket: params.bucket, rootDirectory: rootDirectory, email: params.email, privateKey: params.privateKey, client: params.client, chunkSize: params.chunkSize, } return &base.Base{ StorageDriver: d, }, nil } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. // This should primarily be used for small objects. func (d *driver) GetContent(context ctx.Context, path string) ([]byte, error) { gcsContext := d.context(context) name := d.pathToKey(path) var rc io.ReadCloser err := retry(func() error { var err error rc, err = storage.NewReader(gcsContext, d.bucket, name) return err }) if err == storage.ErrObjectNotExist { return nil, storagedriver.PathNotFoundError{Path: path} } if err != nil { return nil, err } defer rc.Close() p, err := ioutil.ReadAll(rc) if err != nil { return nil, err } return p, nil } // PutContent stores the []byte content at a location designated by "path". // This should primarily be used for small objects. func (d *driver) PutContent(context ctx.Context, path string, contents []byte) error { return retry(func() error { wc := storage.NewWriter(d.context(context), d.bucket, d.pathToKey(path)) wc.ContentType = "application/octet-stream" return putContentsClose(wc, contents) }) } // Reader retrieves an io.ReadCloser for the content stored at "path" // with a given byte offset. // May be used to resume reading a stream by providing a nonzero offset. func (d *driver) Reader(context ctx.Context, path string, offset int64) (io.ReadCloser, error) { res, err := getObject(d.client, d.bucket, d.pathToKey(path), offset) if err != nil { if res != nil { if res.StatusCode == http.StatusNotFound { res.Body.Close() return nil, storagedriver.PathNotFoundError{Path: path} } if res.StatusCode == http.StatusRequestedRangeNotSatisfiable { res.Body.Close() obj, err := storageStatObject(d.context(context), d.bucket, d.pathToKey(path)) if err != nil { return nil, err } if offset == int64(obj.Size) { return ioutil.NopCloser(bytes.NewReader([]byte{})), nil } return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset} } } return nil, err } if res.Header.Get("Content-Type") == uploadSessionContentType { defer res.Body.Close() return nil, storagedriver.PathNotFoundError{Path: path} } return res.Body, nil } func getObject(client *http.Client, bucket string, name string, offset int64) (*http.Response, error) { // copied from google.golang.org/cloud/storage#NewReader : // to set the additional "Range" header u := &url.URL{ Scheme: "https", Host: "storage.googleapis.com", Path: fmt.Sprintf("/%s/%s", bucket, name), } req, err := http.NewRequest("GET", u.String(), nil) if err != nil { return nil, err } if offset > 0 { req.Header.Set("Range", fmt.Sprintf("bytes=%v-", offset)) } var res *http.Response err = retry(func() error { var err error res, err = client.Do(req) return err }) if err != nil { return nil, err } return res, googleapi.CheckMediaResponse(res) } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(context ctx.Context, path string, append bool) (storagedriver.FileWriter, error) { writer := &writer{ client: d.client, bucket: d.bucket, name: d.pathToKey(path), buffer: make([]byte, d.chunkSize), } if append { err := writer.init(path) if err != nil { return nil, err } } return writer, nil } type writer struct { client *http.Client bucket string name string size int64 offset int64 closed bool sessionURI string buffer []byte buffSize int } // Cancel removes any written content from this FileWriter. func (w *writer) Cancel() error { w.closed = true err := storageDeleteObject(cloud.NewContext(dummyProjectID, w.client), w.bucket, w.name) if err != nil { if status, ok := err.(*googleapi.Error); ok { if status.Code == http.StatusNotFound { err = nil } } } return err } func (w *writer) Close() error { if w.closed { return nil } w.closed = true err := w.writeChunk() if err != nil { return err } // Copy the remaining bytes from the buffer to the upload session // Normally buffSize will be smaller than minChunkSize. However, in the // unlikely event that the upload session failed to start, this number could be higher. // In this case we can safely clip the remaining bytes to the minChunkSize if w.buffSize > minChunkSize { w.buffSize = minChunkSize } // commit the writes by updating the upload session err = retry(func() error { wc := storage.NewWriter(cloud.NewContext(dummyProjectID, w.client), w.bucket, w.name) wc.ContentType = uploadSessionContentType wc.Metadata = map[string]string{ "Session-URI": w.sessionURI, "Offset": strconv.FormatInt(w.offset, 10), } return putContentsClose(wc, w.buffer[0:w.buffSize]) }) if err != nil { return err } w.size = w.offset + int64(w.buffSize) w.buffSize = 0 return nil } func putContentsClose(wc *storage.Writer, contents []byte) error { size := len(contents) var nn int var err error for nn < size { n, err := wc.Write(contents[nn:size]) nn += n if err != nil { break } } if err != nil { wc.CloseWithError(err) return err } return wc.Close() } // Commit flushes all content written to this FileWriter and makes it // available for future calls to StorageDriver.GetContent and // StorageDriver.Reader. func (w *writer) Commit() error { if err := w.checkClosed(); err != nil { return err } w.closed = true // no session started yet just perform a simple upload if w.sessionURI == "" { err := retry(func() error { wc := storage.NewWriter(cloud.NewContext(dummyProjectID, w.client), w.bucket, w.name) wc.ContentType = "application/octet-stream" return putContentsClose(wc, w.buffer[0:w.buffSize]) }) if err != nil { return err } w.size = w.offset + int64(w.buffSize) w.buffSize = 0 return nil } size := w.offset + int64(w.buffSize) var nn int // loop must be performed at least once to ensure the file is committed even when // the buffer is empty for { n, err := putChunk(w.client, w.sessionURI, w.buffer[nn:w.buffSize], w.offset, size) nn += int(n) w.offset += n w.size = w.offset if err != nil { w.buffSize = copy(w.buffer, w.buffer[nn:w.buffSize]) return err } if nn == w.buffSize { break } } w.buffSize = 0 return nil } func (w *writer) checkClosed() error { if w.closed { return fmt.Errorf("Writer already closed") } return nil } func (w *writer) writeChunk() error { var err error // chunks can be uploaded only in multiples of minChunkSize // chunkSize is a multiple of minChunkSize less than or equal to buffSize chunkSize := w.buffSize - (w.buffSize % minChunkSize) if chunkSize == 0 { return nil } // if their is no sessionURI yet, obtain one by starting the session if w.sessionURI == "" { w.sessionURI, err = startSession(w.client, w.bucket, w.name) } if err != nil { return err } nn, err := putChunk(w.client, w.sessionURI, w.buffer[0:chunkSize], w.offset, -1) w.offset += nn if w.offset > w.size { w.size = w.offset } // shift the remaining bytes to the start of the buffer w.buffSize = copy(w.buffer, w.buffer[int(nn):w.buffSize]) return err } func (w *writer) Write(p []byte) (int, error) { err := w.checkClosed() if err != nil { return 0, err } var nn int for nn < len(p) { n := copy(w.buffer[w.buffSize:], p[nn:]) w.buffSize += n if w.buffSize == cap(w.buffer) { err = w.writeChunk() if err != nil { break } } nn += n } return nn, err } // Size returns the number of bytes written to this FileWriter. func (w *writer) Size() int64 { return w.size } func (w *writer) init(path string) error { res, err := getObject(w.client, w.bucket, w.name, 0) if err != nil { return err } defer res.Body.Close() if res.Header.Get("Content-Type") != uploadSessionContentType { return storagedriver.PathNotFoundError{Path: path} } offset, err := strconv.ParseInt(res.Header.Get("X-Goog-Meta-Offset"), 10, 64) if err != nil { return err } buffer, err := ioutil.ReadAll(res.Body) if err != nil { return err } w.sessionURI = res.Header.Get("X-Goog-Meta-Session-URI") w.buffSize = copy(w.buffer, buffer) w.offset = offset w.size = offset + int64(w.buffSize) return nil } type request func() error func retry(req request) error { backoff := time.Second var err error for i := 0; i < maxTries; i++ { err = req() if err == nil { return nil } status, ok := err.(*googleapi.Error) if !ok || (status.Code != 429 && status.Code < http.StatusInternalServerError) { return err } time.Sleep(backoff - time.Second + (time.Duration(rand.Int31n(1000)) * time.Millisecond)) if i <= 4 { backoff = backoff * 2 } } return err } // Stat retrieves the FileInfo for the given path, including the current // size in bytes and the creation time. func (d *driver) Stat(context ctx.Context, path string) (storagedriver.FileInfo, error) { var fi storagedriver.FileInfoFields //try to get as file gcsContext := d.context(context) obj, err := storageStatObject(gcsContext, d.bucket, d.pathToKey(path)) if err == nil { if obj.ContentType == uploadSessionContentType { return nil, storagedriver.PathNotFoundError{Path: path} } fi = storagedriver.FileInfoFields{ Path: path, Size: obj.Size, ModTime: obj.Updated, IsDir: false, } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } //try to get as folder dirpath := d.pathToDirKey(path) var query *storage.Query query = &storage.Query{} query.Prefix = dirpath query.MaxResults = 1 objects, err := storageListObjects(gcsContext, d.bucket, query) if err != nil { return nil, err } if len(objects.Results) < 1 { return nil, storagedriver.PathNotFoundError{Path: path} } fi = storagedriver.FileInfoFields{ Path: path, IsDir: true, } obj = objects.Results[0] if obj.Name == dirpath { fi.Size = obj.Size fi.ModTime = obj.Updated } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } // List returns a list of the objects that are direct descendants of the //given path. func (d *driver) List(context ctx.Context, path string) ([]string, error) { var query *storage.Query query = &storage.Query{} query.Delimiter = "/" query.Prefix = d.pathToDirKey(path) list := make([]string, 0, 64) for { objects, err := storageListObjects(d.context(context), d.bucket, query) if err != nil { return nil, err } for _, object := range objects.Results { // GCS does not guarantee strong consistency between // DELETE and LIST operations. Check that the object is not deleted, // and filter out any objects with a non-zero time-deleted if object.Deleted.IsZero() && object.ContentType != uploadSessionContentType { list = append(list, d.keyToPath(object.Name)) } } for _, subpath := range objects.Prefixes { subpath = d.keyToPath(subpath) list = append(list, subpath) } query = objects.Next if query == nil { break } } if path != "/" && len(list) == 0 { // Treat empty response as missing directory, since we don't actually // have directories in Google Cloud Storage. return nil, storagedriver.PathNotFoundError{Path: path} } return list, nil } // Move moves an object stored at sourcePath to destPath, removing the // original object. func (d *driver) Move(context ctx.Context, sourcePath string, destPath string) error { gcsContext := d.context(context) _, err := storageCopyObject(gcsContext, d.bucket, d.pathToKey(sourcePath), d.bucket, d.pathToKey(destPath), nil) if err != nil { if status, ok := err.(*googleapi.Error); ok { if status.Code == http.StatusNotFound { return storagedriver.PathNotFoundError{Path: sourcePath} } } return err } err = storageDeleteObject(gcsContext, d.bucket, d.pathToKey(sourcePath)) // if deleting the file fails, log the error, but do not fail; the file was successfully copied, // and the original should eventually be cleaned when purging the uploads folder. if err != nil { logrus.Infof("error deleting file: %v due to %v", sourcePath, err) } return nil } // listAll recursively lists all names of objects stored at "prefix" and its subpaths. func (d *driver) listAll(context context.Context, prefix string) ([]string, error) { list := make([]string, 0, 64) query := &storage.Query{} query.Prefix = prefix query.Versions = false for { objects, err := storageListObjects(d.context(context), d.bucket, query) if err != nil { return nil, err } for _, obj := range objects.Results { // GCS does not guarantee strong consistency between // DELETE and LIST operations. Check that the object is not deleted, // and filter out any objects with a non-zero time-deleted if obj.Deleted.IsZero() { list = append(list, obj.Name) } } query = objects.Next if query == nil { break } } return list, nil } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(context ctx.Context, path string) error { prefix := d.pathToDirKey(path) gcsContext := d.context(context) keys, err := d.listAll(gcsContext, prefix) if err != nil { return err } if len(keys) > 0 { sort.Sort(sort.Reverse(sort.StringSlice(keys))) for _, key := range keys { err := storageDeleteObject(gcsContext, d.bucket, key) // GCS only guarantees eventual consistency, so listAll might return // paths that no longer exist. If this happens, just ignore any not // found error if status, ok := err.(*googleapi.Error); ok { if status.Code == http.StatusNotFound { err = nil } } if err != nil { return err } } return nil } err = storageDeleteObject(gcsContext, d.bucket, d.pathToKey(path)) if err != nil { if status, ok := err.(*googleapi.Error); ok { if status.Code == http.StatusNotFound { return storagedriver.PathNotFoundError{Path: path} } } } return err } func storageDeleteObject(context context.Context, bucket string, name string) error { return retry(func() error { return storage.DeleteObject(context, bucket, name) }) } func storageStatObject(context context.Context, bucket string, name string) (*storage.Object, error) { var obj *storage.Object err := retry(func() error { var err error obj, err = storage.StatObject(context, bucket, name) return err }) return obj, err } func storageListObjects(context context.Context, bucket string, q *storage.Query) (*storage.Objects, error) { var objs *storage.Objects err := retry(func() error { var err error objs, err = storage.ListObjects(context, bucket, q) return err }) return objs, err } func storageCopyObject(context context.Context, srcBucket, srcName string, destBucket, destName string, attrs *storage.ObjectAttrs) (*storage.Object, error) { var obj *storage.Object err := retry(func() error { var err error obj, err = storage.CopyObject(context, srcBucket, srcName, destBucket, destName, attrs) return err }) return obj, err } // URLFor returns a URL which may be used to retrieve the content stored at // the given path, possibly using the given options. // Returns ErrUnsupportedMethod if this driver has no privateKey func (d *driver) URLFor(context ctx.Context, path string, options map[string]interface{}) (string, error) { if d.privateKey == nil { return "", storagedriver.ErrUnsupportedMethod{} } name := d.pathToKey(path) methodString := "GET" method, ok := options["method"] if ok { methodString, ok = method.(string) if !ok || (methodString != "GET" && methodString != "HEAD") { return "", storagedriver.ErrUnsupportedMethod{} } } expiresTime := time.Now().Add(20 * time.Minute) expires, ok := options["expiry"] if ok { et, ok := expires.(time.Time) if ok { expiresTime = et } } opts := &storage.SignedURLOptions{ GoogleAccessID: d.email, PrivateKey: d.privateKey, Method: methodString, Expires: expiresTime, } return storage.SignedURL(d.bucket, name, opts) } func startSession(client *http.Client, bucket string, name string) (uri string, err error) { u := &url.URL{ Scheme: "https", Host: "www.googleapis.com", Path: fmt.Sprintf("/upload/storage/v1/b/%v/o", bucket), RawQuery: fmt.Sprintf("uploadType=resumable&name=%v", name), } err = retry(func() error { req, err := http.NewRequest("POST", u.String(), nil) if err != nil { return err } req.Header.Set("X-Upload-Content-Type", "application/octet-stream") req.Header.Set("Content-Length", "0") resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() err = googleapi.CheckMediaResponse(resp) if err != nil { return err } uri = resp.Header.Get("Location") return nil }) return uri, err } func putChunk(client *http.Client, sessionURI string, chunk []byte, from int64, totalSize int64) (int64, error) { bytesPut := int64(0) err := retry(func() error { req, err := http.NewRequest("PUT", sessionURI, bytes.NewReader(chunk)) if err != nil { return err } length := int64(len(chunk)) to := from + length - 1 size := "*" if totalSize >= 0 { size = strconv.FormatInt(totalSize, 10) } req.Header.Set("Content-Type", "application/octet-stream") if from == to+1 { req.Header.Set("Content-Range", fmt.Sprintf("bytes */%v", size)) } else { req.Header.Set("Content-Range", fmt.Sprintf("bytes %v-%v/%v", from, to, size)) } req.Header.Set("Content-Length", strconv.FormatInt(length, 10)) resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() if totalSize < 0 && resp.StatusCode == 308 { groups := rangeHeader.FindStringSubmatch(resp.Header.Get("Range")) end, err := strconv.ParseInt(groups[2], 10, 64) if err != nil { return err } bytesPut = end - from + 1 return nil } err = googleapi.CheckMediaResponse(resp) if err != nil { return err } bytesPut = to - from + 1 return nil }) return bytesPut, err } func (d *driver) context(context ctx.Context) context.Context { return cloud.WithContext(context, dummyProjectID, d.client) } func (d *driver) pathToKey(path string) string { return strings.TrimRight(d.rootDirectory+strings.TrimLeft(path, "/"), "/") } func (d *driver) pathToDirKey(path string) string { return d.pathToKey(path) + "/" } func (d *driver) keyToPath(key string) string { return "/" + strings.Trim(strings.TrimPrefix(key, d.rootDirectory), "/") } docker-registry-2.6.2~ds1/registry/storage/driver/gcs/gcs_test.go000066400000000000000000000200571313450123100251660ustar00rootroot00000000000000// +build include_gcs package gcs import ( "io/ioutil" "os" "testing" "fmt" ctx "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/googleapi" "google.golang.org/cloud/storage" "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } var gcsDriverConstructor func(rootDirectory string) (storagedriver.StorageDriver, error) var skipGCS func() string func init() { bucket := os.Getenv("REGISTRY_STORAGE_GCS_BUCKET") credentials := os.Getenv("GOOGLE_APPLICATION_CREDENTIALS") // Skip GCS storage driver tests if environment variable parameters are not provided skipGCS = func() string { if bucket == "" || credentials == "" { return "The following environment variables must be set to enable these tests: REGISTRY_STORAGE_GCS_BUCKET, GOOGLE_APPLICATION_CREDENTIALS" } return "" } if skipGCS() != "" { return } root, err := ioutil.TempDir("", "driver-") if err != nil { panic(err) } defer os.Remove(root) var ts oauth2.TokenSource var email string var privateKey []byte ts, err = google.DefaultTokenSource(ctx.Background(), storage.ScopeFullControl) if err != nil { // Assume that the file contents are within the environment variable since it exists // but does not contain a valid file path jwtConfig, err := google.JWTConfigFromJSON([]byte(credentials), storage.ScopeFullControl) if err != nil { panic(fmt.Sprintf("Error reading JWT config : %s", err)) } email = jwtConfig.Email privateKey = []byte(jwtConfig.PrivateKey) if len(privateKey) == 0 { panic("Error reading JWT config : missing private_key property") } if email == "" { panic("Error reading JWT config : missing client_email property") } ts = jwtConfig.TokenSource(ctx.Background()) } gcsDriverConstructor = func(rootDirectory string) (storagedriver.StorageDriver, error) { parameters := driverParameters{ bucket: bucket, rootDirectory: root, email: email, privateKey: privateKey, client: oauth2.NewClient(ctx.Background(), ts), chunkSize: defaultChunkSize, } return New(parameters) } testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) { return gcsDriverConstructor(root) }, skipGCS) } // Test Committing a FileWriter without having called Write func TestCommitEmpty(t *testing.T) { if skipGCS() != "" { t.Skip(skipGCS()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) driver, err := gcsDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } filename := "/test" ctx := ctx.Background() writer, err := driver.Writer(ctx, filename, false) defer driver.Delete(ctx, filename) if err != nil { t.Fatalf("driver.Writer: unexpected error: %v", err) } err = writer.Commit() if err != nil { t.Fatalf("writer.Commit: unexpected error: %v", err) } err = writer.Close() if err != nil { t.Fatalf("writer.Close: unexpected error: %v", err) } if writer.Size() != 0 { t.Fatalf("writer.Size: %d != 0", writer.Size()) } readContents, err := driver.GetContent(ctx, filename) if err != nil { t.Fatalf("driver.GetContent: unexpected error: %v", err) } if len(readContents) != 0 { t.Fatalf("len(driver.GetContent(..)): %d != 0", len(readContents)) } } // Test Committing a FileWriter after having written exactly // defaultChunksize bytes. func TestCommit(t *testing.T) { if skipGCS() != "" { t.Skip(skipGCS()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) driver, err := gcsDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } filename := "/test" ctx := ctx.Background() contents := make([]byte, defaultChunkSize) writer, err := driver.Writer(ctx, filename, false) defer driver.Delete(ctx, filename) if err != nil { t.Fatalf("driver.Writer: unexpected error: %v", err) } _, err = writer.Write(contents) if err != nil { t.Fatalf("writer.Write: unexpected error: %v", err) } err = writer.Commit() if err != nil { t.Fatalf("writer.Commit: unexpected error: %v", err) } err = writer.Close() if err != nil { t.Fatalf("writer.Close: unexpected error: %v", err) } if writer.Size() != int64(len(contents)) { t.Fatalf("writer.Size: %d != %d", writer.Size(), len(contents)) } readContents, err := driver.GetContent(ctx, filename) if err != nil { t.Fatalf("driver.GetContent: unexpected error: %v", err) } if len(readContents) != len(contents) { t.Fatalf("len(driver.GetContent(..)): %d != %d", len(readContents), len(contents)) } } func TestRetry(t *testing.T) { if skipGCS() != "" { t.Skip(skipGCS()) } assertError := func(expected string, observed error) { observedMsg := "" if observed != nil { observedMsg = observed.Error() } if observedMsg != expected { t.Fatalf("expected %v, observed %v\n", expected, observedMsg) } } err := retry(func() error { return &googleapi.Error{ Code: 503, Message: "google api error", } }) assertError("googleapi: Error 503: google api error", err) err = retry(func() error { return &googleapi.Error{ Code: 404, Message: "google api error", } }) assertError("googleapi: Error 404: google api error", err) err = retry(func() error { return fmt.Errorf("error") }) assertError("error", err) } func TestEmptyRootList(t *testing.T) { if skipGCS() != "" { t.Skip(skipGCS()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) rootedDriver, err := gcsDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } emptyRootDriver, err := gcsDriverConstructor("") if err != nil { t.Fatalf("unexpected error creating empty root driver: %v", err) } slashRootDriver, err := gcsDriverConstructor("/") if err != nil { t.Fatalf("unexpected error creating slash root driver: %v", err) } filename := "/test" contents := []byte("contents") ctx := ctx.Background() err = rootedDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer func() { err := rootedDriver.Delete(ctx, filename) if err != nil { t.Fatalf("failed to remove %v due to %v\n", filename, err) } }() keys, err := emptyRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } keys, err = slashRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } } // TestMoveDirectory checks that moving a directory returns an error. func TestMoveDirectory(t *testing.T) { if skipGCS() != "" { t.Skip(skipGCS()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) driver, err := gcsDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } ctx := ctx.Background() contents := []byte("contents") // Create a regular file. err = driver.PutContent(ctx, "/parent/dir/foo", contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer func() { err := driver.Delete(ctx, "/parent") if err != nil { t.Fatalf("failed to remove /parent due to %v\n", err) } }() err = driver.Move(ctx, "/parent/dir", "/parent/other") if err == nil { t.Fatalf("Moving directory /parent/dir /parent/other should have return a non-nil error\n") } } docker-registry-2.6.2~ds1/registry/storage/driver/inmemory/000077500000000000000000000000001313450123100241035ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/inmemory/driver.go000066400000000000000000000156451313450123100257400ustar00rootroot00000000000000package inmemory import ( "fmt" "io" "io/ioutil" "sync" "time" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const driverName = "inmemory" func init() { factory.Register(driverName, &inMemoryDriverFactory{}) } // inMemoryDriverFacotry implements the factory.StorageDriverFactory interface. type inMemoryDriverFactory struct{} func (factory *inMemoryDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return New(), nil } type driver struct { root *dir mutex sync.RWMutex } // baseEmbed allows us to hide the Base embed. type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by a local map. // Intended solely for example and testing purposes. type Driver struct { baseEmbed // embedded, hidden base driver. } var _ storagedriver.StorageDriver = &Driver{} // New constructs a new Driver. func New() *Driver { return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: &driver{ root: &dir{ common: common{ p: "/", mod: time.Now(), }, }, }, }, }, } } // Implement the storagedriver.StorageDriver interface. func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { d.mutex.RLock() defer d.mutex.RUnlock() rc, err := d.Reader(ctx, path, 0) if err != nil { return nil, err } defer rc.Close() return ioutil.ReadAll(rc) } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, p string, contents []byte) error { d.mutex.Lock() defer d.mutex.Unlock() normalized := normalize(p) f, err := d.root.mkfile(normalized) if err != nil { // TODO(stevvooe): Again, we need to clarify when this is not a // directory in StorageDriver API. return fmt.Errorf("not a file") } f.truncate() f.WriteAt(contents, 0) return nil } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { d.mutex.RLock() defer d.mutex.RUnlock() if offset < 0 { return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset} } normalized := normalize(path) found := d.root.find(normalized) if found.path() != normalized { return nil, storagedriver.PathNotFoundError{Path: path} } if found.isdir() { return nil, fmt.Errorf("%q is a directory", path) } return ioutil.NopCloser(found.(*file).sectionReader(offset)), nil } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { d.mutex.Lock() defer d.mutex.Unlock() normalized := normalize(path) f, err := d.root.mkfile(normalized) if err != nil { return nil, fmt.Errorf("not a file") } if !append { f.truncate() } return d.newWriter(f), nil } // Stat returns info about the provided path. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { d.mutex.RLock() defer d.mutex.RUnlock() normalized := normalize(path) found := d.root.find(normalized) if found.path() != normalized { return nil, storagedriver.PathNotFoundError{Path: path} } fi := storagedriver.FileInfoFields{ Path: path, IsDir: found.isdir(), ModTime: found.modtime(), } if !fi.IsDir { fi.Size = int64(len(found.(*file).data)) } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } // List returns a list of the objects that are direct descendants of the given // path. func (d *driver) List(ctx context.Context, path string) ([]string, error) { d.mutex.RLock() defer d.mutex.RUnlock() normalized := normalize(path) found := d.root.find(normalized) if !found.isdir() { return nil, fmt.Errorf("not a directory") // TODO(stevvooe): Need error type for this... } entries, err := found.(*dir).list(normalized) if err != nil { switch err { case errNotExists: return nil, storagedriver.PathNotFoundError{Path: path} case errIsNotDir: return nil, fmt.Errorf("not a directory") default: return nil, err } } return entries, nil } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { d.mutex.Lock() defer d.mutex.Unlock() normalizedSrc, normalizedDst := normalize(sourcePath), normalize(destPath) err := d.root.move(normalizedSrc, normalizedDst) switch err { case errNotExists: return storagedriver.PathNotFoundError{Path: destPath} default: return err } } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, path string) error { d.mutex.Lock() defer d.mutex.Unlock() normalized := normalize(path) err := d.root.delete(normalized) switch err { case errNotExists: return storagedriver.PathNotFoundError{Path: path} default: return err } } // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { return "", storagedriver.ErrUnsupportedMethod{} } type writer struct { d *driver f *file closed bool committed bool cancelled bool } func (d *driver) newWriter(f *file) storagedriver.FileWriter { return &writer{ d: d, f: f, } } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } w.d.mutex.Lock() defer w.d.mutex.Unlock() return w.f.WriteAt(p, int64(len(w.f.data))) } func (w *writer) Size() int64 { w.d.mutex.RLock() defer w.d.mutex.RUnlock() return int64(len(w.f.data)) } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } w.closed = true return nil } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true w.d.mutex.Lock() defer w.d.mutex.Unlock() return w.d.root.delete(w.f.path()) } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } w.committed = true return nil } docker-registry-2.6.2~ds1/registry/storage/driver/inmemory/driver_test.go000066400000000000000000000007471313450123100267740ustar00rootroot00000000000000package inmemory import ( "testing" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } func init() { inmemoryDriverConstructor := func() (storagedriver.StorageDriver, error) { return New(), nil } testsuites.RegisterSuite(inmemoryDriverConstructor, testsuites.NeverSkip) } docker-registry-2.6.2~ds1/registry/storage/driver/inmemory/mfs.go000066400000000000000000000137501313450123100252250ustar00rootroot00000000000000package inmemory import ( "fmt" "io" "path" "sort" "strings" "time" ) var ( errExists = fmt.Errorf("exists") errNotExists = fmt.Errorf("notexists") errIsNotDir = fmt.Errorf("notdir") errIsDir = fmt.Errorf("isdir") ) type node interface { name() string path() string isdir() bool modtime() time.Time } // dir is the central type for the memory-based storagedriver. All operations // are dispatched from a root dir. type dir struct { common // TODO(stevvooe): Use sorted slice + search. children map[string]node } var _ node = &dir{} func (d *dir) isdir() bool { return true } // add places the node n into dir d. func (d *dir) add(n node) { if d.children == nil { d.children = make(map[string]node) } d.children[n.name()] = n d.mod = time.Now() } // find searches for the node, given path q in dir. If the node is found, it // will be returned. If the node is not found, the closet existing parent. If // the node is found, the returned (node).path() will match q. func (d *dir) find(q string) node { q = strings.Trim(q, "/") i := strings.Index(q, "/") if q == "" { return d } if i == 0 { panic("shouldn't happen, no root paths") } var component string if i < 0 { // No more path components component = q } else { component = q[:i] } child, ok := d.children[component] if !ok { // Node was not found. Return p and the current node. return d } if child.isdir() { // traverse down! q = q[i+1:] return child.(*dir).find(q) } return child } func (d *dir) list(p string) ([]string, error) { n := d.find(p) if n.path() != p { return nil, errNotExists } if !n.isdir() { return nil, errIsNotDir } var children []string for _, child := range n.(*dir).children { children = append(children, child.path()) } sort.Strings(children) return children, nil } // mkfile or return the existing one. returns an error if it exists and is a // directory. Essentially, this is open or create. func (d *dir) mkfile(p string) (*file, error) { n := d.find(p) if n.path() == p { if n.isdir() { return nil, errIsDir } return n.(*file), nil } dirpath, filename := path.Split(p) // Make any non-existent directories n, err := d.mkdirs(dirpath) if err != nil { return nil, err } dd := n.(*dir) n = &file{ common: common{ p: path.Join(dd.path(), filename), mod: time.Now(), }, } dd.add(n) return n.(*file), nil } // mkdirs creates any missing directory entries in p and returns the result. func (d *dir) mkdirs(p string) (*dir, error) { p = normalize(p) n := d.find(p) if !n.isdir() { // Found something there return nil, errIsNotDir } if n.path() == p { return n.(*dir), nil } dd := n.(*dir) relative := strings.Trim(strings.TrimPrefix(p, n.path()), "/") if relative == "" { return dd, nil } components := strings.Split(relative, "/") for _, component := range components { d, err := dd.mkdir(component) if err != nil { // This should actually never happen, since there are no children. return nil, err } dd = d } return dd, nil } // mkdir creates a child directory under d with the given name. func (d *dir) mkdir(name string) (*dir, error) { if name == "" { return nil, fmt.Errorf("invalid dirname") } _, ok := d.children[name] if ok { return nil, errExists } child := &dir{ common: common{ p: path.Join(d.path(), name), mod: time.Now(), }, } d.add(child) d.mod = time.Now() return child, nil } func (d *dir) move(src, dst string) error { dstDirname, _ := path.Split(dst) dp, err := d.mkdirs(dstDirname) if err != nil { return err } srcDirname, srcFilename := path.Split(src) sp := d.find(srcDirname) if normalize(srcDirname) != normalize(sp.path()) { return errNotExists } spd, ok := sp.(*dir) if !ok { return errIsNotDir // paranoid. } s, ok := spd.children[srcFilename] if !ok { return errNotExists } delete(spd.children, srcFilename) switch n := s.(type) { case *dir: n.p = dst case *file: n.p = dst } dp.add(s) return nil } func (d *dir) delete(p string) error { dirname, filename := path.Split(p) parent := d.find(dirname) if normalize(dirname) != normalize(parent.path()) { return errNotExists } if _, ok := parent.(*dir).children[filename]; !ok { return errNotExists } delete(parent.(*dir).children, filename) return nil } // dump outputs a primitive directory structure to stdout. func (d *dir) dump(indent string) { fmt.Println(indent, d.name()+"/") for _, child := range d.children { if child.isdir() { child.(*dir).dump(indent + "\t") } else { fmt.Println(indent, child.name()) } } } func (d *dir) String() string { return fmt.Sprintf("&dir{path: %v, children: %v}", d.p, d.children) } // file stores actual data in the fs tree. It acts like an open, seekable file // where operations are conducted through ReadAt and WriteAt. Use it with // SectionReader for the best effect. type file struct { common data []byte } var _ node = &file{} func (f *file) isdir() bool { return false } func (f *file) truncate() { f.data = f.data[:0] } func (f *file) sectionReader(offset int64) io.Reader { return io.NewSectionReader(f, offset, int64(len(f.data))-offset) } func (f *file) ReadAt(p []byte, offset int64) (n int, err error) { return copy(p, f.data[offset:]), nil } func (f *file) WriteAt(p []byte, offset int64) (n int, err error) { off := int(offset) if cap(f.data) < off+len(p) { data := make([]byte, len(f.data), off+len(p)) copy(data, f.data) f.data = data } f.mod = time.Now() f.data = f.data[:off+len(p)] return copy(f.data[off:off+len(p)], p), nil } func (f *file) String() string { return fmt.Sprintf("&file{path: %q}", f.p) } // common provides shared fields and methods for node implementations. type common struct { p string mod time.Time } func (c *common) name() string { _, name := path.Split(c.p) return name } func (c *common) path() string { return c.p } func (c *common) modtime() time.Time { return c.mod } func normalize(p string) string { return "/" + strings.Trim(p, "/") } docker-registry-2.6.2~ds1/registry/storage/driver/middleware/000077500000000000000000000000001313450123100243615ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/middleware/cloudfront/000077500000000000000000000000001313450123100265405ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/middleware/cloudfront/middleware.go000066400000000000000000000076261313450123100312170ustar00rootroot00000000000000// Package middleware - cloudfront wrapper for storage libs // N.B. currently only works with S3, not arbitrary sites // package middleware import ( "crypto/x509" "encoding/pem" "fmt" "io/ioutil" "net/url" "strings" "time" "github.com/aws/aws-sdk-go/service/cloudfront/sign" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" ) // cloudFrontStorageMiddleware provides a simple implementation of layerHandler that // constructs temporary signed CloudFront URLs from the storagedriver layer URL, // then issues HTTP Temporary Redirects to this CloudFront content URL. type cloudFrontStorageMiddleware struct { storagedriver.StorageDriver urlSigner *sign.URLSigner baseURL string duration time.Duration } var _ storagedriver.StorageDriver = &cloudFrontStorageMiddleware{} // newCloudFrontLayerHandler constructs and returns a new CloudFront // LayerHandler implementation. // Required options: baseurl, privatekey, keypairid func newCloudFrontStorageMiddleware(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (storagedriver.StorageDriver, error) { base, ok := options["baseurl"] if !ok { return nil, fmt.Errorf("no baseurl provided") } baseURL, ok := base.(string) if !ok { return nil, fmt.Errorf("baseurl must be a string") } if !strings.Contains(baseURL, "://") { baseURL = "https://" + baseURL } if !strings.HasSuffix(baseURL, "/") { baseURL += "/" } if _, err := url.Parse(baseURL); err != nil { return nil, fmt.Errorf("invalid baseurl: %v", err) } pk, ok := options["privatekey"] if !ok { return nil, fmt.Errorf("no privatekey provided") } pkPath, ok := pk.(string) if !ok { return nil, fmt.Errorf("privatekey must be a string") } kpid, ok := options["keypairid"] if !ok { return nil, fmt.Errorf("no keypairid provided") } keypairID, ok := kpid.(string) if !ok { return nil, fmt.Errorf("keypairid must be a string") } pkBytes, err := ioutil.ReadFile(pkPath) if err != nil { return nil, fmt.Errorf("failed to read privatekey file: %s", err) } block, _ := pem.Decode([]byte(pkBytes)) if block == nil { return nil, fmt.Errorf("failed to decode private key as an rsa private key") } privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return nil, err } urlSigner := sign.NewURLSigner(keypairID, privateKey) duration := 20 * time.Minute d, ok := options["duration"] if ok { switch d := d.(type) { case time.Duration: duration = d case string: dur, err := time.ParseDuration(d) if err != nil { return nil, fmt.Errorf("invalid duration: %s", err) } duration = dur } } return &cloudFrontStorageMiddleware{ StorageDriver: storageDriver, urlSigner: urlSigner, baseURL: baseURL, duration: duration, }, nil } // S3BucketKeyer is any type that is capable of returning the S3 bucket key // which should be cached by AWS CloudFront. type S3BucketKeyer interface { S3BucketKey(path string) string } // Resolve returns an http.Handler which can serve the contents of the given // Layer, or an error if not supported by the storagedriver. func (lh *cloudFrontStorageMiddleware) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { // TODO(endophage): currently only supports S3 keyer, ok := lh.StorageDriver.(S3BucketKeyer) if !ok { context.GetLogger(ctx).Warn("the CloudFront middleware does not support this backend storage driver") return lh.StorageDriver.URLFor(ctx, path, options) } cfURL, err := lh.urlSigner.Sign(lh.baseURL+keyer.S3BucketKey(path), time.Now().Add(lh.duration)) if err != nil { return "", err } return cfURL, nil } // init registers the cloudfront layerHandler backend. func init() { storagemiddleware.Register("cloudfront", storagemiddleware.InitFunc(newCloudFrontStorageMiddleware)) } docker-registry-2.6.2~ds1/registry/storage/driver/middleware/redirect/000077500000000000000000000000001313450123100261625ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/middleware/redirect/middleware.go000066400000000000000000000026521313450123100306330ustar00rootroot00000000000000package middleware import ( "fmt" "net/url" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" storagemiddleware "github.com/docker/distribution/registry/storage/driver/middleware" ) type redirectStorageMiddleware struct { storagedriver.StorageDriver scheme string host string } var _ storagedriver.StorageDriver = &redirectStorageMiddleware{} func newRedirectStorageMiddleware(sd storagedriver.StorageDriver, options map[string]interface{}) (storagedriver.StorageDriver, error) { o, ok := options["baseurl"] if !ok { return nil, fmt.Errorf("no baseurl provided") } b, ok := o.(string) if !ok { return nil, fmt.Errorf("baseurl must be a string") } u, err := url.Parse(b) if err != nil { return nil, fmt.Errorf("unable to parse redirect baseurl: %s", b) } if u.Scheme == "" { return nil, fmt.Errorf("no scheme specified for redirect baseurl") } if u.Host == "" { return nil, fmt.Errorf("no host specified for redirect baseurl") } return &redirectStorageMiddleware{StorageDriver: sd, scheme: u.Scheme, host: u.Host}, nil } func (r *redirectStorageMiddleware) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { u := &url.URL{Scheme: r.scheme, Host: r.host, Path: path} return u.String(), nil } func init() { storagemiddleware.Register("redirect", storagemiddleware.InitFunc(newRedirectStorageMiddleware)) } docker-registry-2.6.2~ds1/registry/storage/driver/middleware/redirect/middleware_test.go000066400000000000000000000034211313450123100316650ustar00rootroot00000000000000package middleware import ( "testing" check "gopkg.in/check.v1" ) func Test(t *testing.T) { check.TestingT(t) } type MiddlewareSuite struct{} var _ = check.Suite(&MiddlewareSuite{}) func (s *MiddlewareSuite) TestNoConfig(c *check.C) { options := make(map[string]interface{}) _, err := newRedirectStorageMiddleware(nil, options) c.Assert(err, check.ErrorMatches, "no baseurl provided") } func (s *MiddlewareSuite) TestMissingScheme(c *check.C) { options := make(map[string]interface{}) options["baseurl"] = "example.com" _, err := newRedirectStorageMiddleware(nil, options) c.Assert(err, check.ErrorMatches, "no scheme specified for redirect baseurl") } func (s *MiddlewareSuite) TestHttpsPort(c *check.C) { options := make(map[string]interface{}) options["baseurl"] = "https://example.com:5443" middleware, err := newRedirectStorageMiddleware(nil, options) c.Assert(err, check.Equals, nil) m, ok := middleware.(*redirectStorageMiddleware) c.Assert(ok, check.Equals, true) c.Assert(m.scheme, check.Equals, "https") c.Assert(m.host, check.Equals, "example.com:5443") url, err := middleware.URLFor(nil, "/rick/data", nil) c.Assert(err, check.Equals, nil) c.Assert(url, check.Equals, "https://example.com:5443/rick/data") } func (s *MiddlewareSuite) TestHTTP(c *check.C) { options := make(map[string]interface{}) options["baseurl"] = "http://example.com" middleware, err := newRedirectStorageMiddleware(nil, options) c.Assert(err, check.Equals, nil) m, ok := middleware.(*redirectStorageMiddleware) c.Assert(ok, check.Equals, true) c.Assert(m.scheme, check.Equals, "http") c.Assert(m.host, check.Equals, "example.com") url, err := middleware.URLFor(nil, "morty/data", nil) c.Assert(err, check.Equals, nil) c.Assert(url, check.Equals, "http://example.com/morty/data") } docker-registry-2.6.2~ds1/registry/storage/driver/middleware/storagemiddleware.go000066400000000000000000000024171313450123100304160ustar00rootroot00000000000000package storagemiddleware import ( "fmt" storagedriver "github.com/docker/distribution/registry/storage/driver" ) // InitFunc is the type of a StorageMiddleware factory function and is // used to register the constructor for different StorageMiddleware backends. type InitFunc func(storageDriver storagedriver.StorageDriver, options map[string]interface{}) (storagedriver.StorageDriver, error) var storageMiddlewares map[string]InitFunc // Register is used to register an InitFunc for // a StorageMiddleware backend with the given name. func Register(name string, initFunc InitFunc) error { if storageMiddlewares == nil { storageMiddlewares = make(map[string]InitFunc) } if _, exists := storageMiddlewares[name]; exists { return fmt.Errorf("name already registered: %s", name) } storageMiddlewares[name] = initFunc return nil } // Get constructs a StorageMiddleware with the given options using the named backend. func Get(name string, options map[string]interface{}, storageDriver storagedriver.StorageDriver) (storagedriver.StorageDriver, error) { if storageMiddlewares != nil { if initFunc, exists := storageMiddlewares[name]; exists { return initFunc(storageDriver, options) } } return nil, fmt.Errorf("no storage middleware registered with name: %s", name) } docker-registry-2.6.2~ds1/registry/storage/driver/oss/000077500000000000000000000000001313450123100230505ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/oss/doc.go000066400000000000000000000002211313450123100241370ustar00rootroot00000000000000// Package oss implements the Aliyun OSS Storage driver backend. Support can be // enabled by including the "include_oss" build tag. package oss docker-registry-2.6.2~ds1/registry/storage/driver/oss/oss.go000066400000000000000000000444711313450123100242150ustar00rootroot00000000000000// Package oss provides a storagedriver.StorageDriver implementation to // store blobs in Aliyun OSS cloud storage. // // This package leverages the denverdino/aliyungo client library for interfacing with // oss. // // Because OSS is a key, value store the Stat call does not support last modification // time for directories (directories are an abstraction for key, value stores) // // +build include_oss package oss import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "reflect" "strconv" "strings" "time" "github.com/docker/distribution/context" "github.com/Sirupsen/logrus" "github.com/denverdino/aliyungo/oss" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const driverName = "oss" // minChunkSize defines the minimum multipart upload chunk size // OSS API requires multipart upload chunks to be at least 5MB const minChunkSize = 5 << 20 const defaultChunkSize = 2 * minChunkSize const defaultTimeout = 2 * time.Minute // 2 minute timeout per chunk // listMax is the largest amount of objects you can request from OSS in a list call const listMax = 1000 //DriverParameters A struct that encapsulates all of the driver parameters after all values have been set type DriverParameters struct { AccessKeyID string AccessKeySecret string Bucket string Region oss.Region Internal bool Encrypt bool Secure bool ChunkSize int64 RootDirectory string Endpoint string } func init() { factory.Register(driverName, &ossDriverFactory{}) } // ossDriverFactory implements the factory.StorageDriverFactory interface type ossDriverFactory struct{} func (factory *ossDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } type driver struct { Client *oss.Client Bucket *oss.Bucket ChunkSize int64 Encrypt bool RootDirectory string } type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by Aliyun OSS // Objects are stored at absolute keys in the provided bucket. type Driver struct { baseEmbed } // FromParameters constructs a new Driver with a given parameters map // Required parameters: // - accesskey // - secretkey // - region // - bucket // - encrypt func FromParameters(parameters map[string]interface{}) (*Driver, error) { // Providing no values for these is valid in case the user is authenticating accessKey, ok := parameters["accesskeyid"] if !ok { return nil, fmt.Errorf("No accesskeyid parameter provided") } secretKey, ok := parameters["accesskeysecret"] if !ok { return nil, fmt.Errorf("No accesskeysecret parameter provided") } regionName, ok := parameters["region"] if !ok || fmt.Sprint(regionName) == "" { return nil, fmt.Errorf("No region parameter provided") } bucket, ok := parameters["bucket"] if !ok || fmt.Sprint(bucket) == "" { return nil, fmt.Errorf("No bucket parameter provided") } internalBool := false internal, ok := parameters["internal"] if ok { internalBool, ok = internal.(bool) if !ok { return nil, fmt.Errorf("The internal parameter should be a boolean") } } encryptBool := false encrypt, ok := parameters["encrypt"] if ok { encryptBool, ok = encrypt.(bool) if !ok { return nil, fmt.Errorf("The encrypt parameter should be a boolean") } } secureBool := true secure, ok := parameters["secure"] if ok { secureBool, ok = secure.(bool) if !ok { return nil, fmt.Errorf("The secure parameter should be a boolean") } } chunkSize := int64(defaultChunkSize) chunkSizeParam, ok := parameters["chunksize"] if ok { switch v := chunkSizeParam.(type) { case string: vv, err := strconv.ParseInt(v, 0, 64) if err != nil { return nil, fmt.Errorf("chunksize parameter must be an integer, %v invalid", chunkSizeParam) } chunkSize = vv case int64: chunkSize = v case int, uint, int32, uint32, uint64: chunkSize = reflect.ValueOf(v).Convert(reflect.TypeOf(chunkSize)).Int() default: return nil, fmt.Errorf("invalid valud for chunksize: %#v", chunkSizeParam) } if chunkSize < minChunkSize { return nil, fmt.Errorf("The chunksize %#v parameter should be a number that is larger than or equal to %d", chunkSize, minChunkSize) } } rootDirectory, ok := parameters["rootdirectory"] if !ok { rootDirectory = "" } endpoint, ok := parameters["endpoint"] if !ok { endpoint = "" } params := DriverParameters{ AccessKeyID: fmt.Sprint(accessKey), AccessKeySecret: fmt.Sprint(secretKey), Bucket: fmt.Sprint(bucket), Region: oss.Region(fmt.Sprint(regionName)), ChunkSize: chunkSize, RootDirectory: fmt.Sprint(rootDirectory), Encrypt: encryptBool, Secure: secureBool, Internal: internalBool, Endpoint: fmt.Sprint(endpoint), } return New(params) } // New constructs a new Driver with the given Aliyun credentials, region, encryption flag, and // bucketName func New(params DriverParameters) (*Driver, error) { client := oss.NewOSSClient(params.Region, params.Internal, params.AccessKeyID, params.AccessKeySecret, params.Secure) client.SetEndpoint(params.Endpoint) bucket := client.Bucket(params.Bucket) client.SetDebug(false) // Validate that the given credentials have at least read permissions in the // given bucket scope. if _, err := bucket.List(strings.TrimRight(params.RootDirectory, "/"), "", "", 1); err != nil { return nil, err } // TODO(tg123): Currently multipart uploads have no timestamps, so this would be unwise // if you initiated a new OSS client while another one is running on the same bucket. d := &driver{ Client: client, Bucket: bucket, ChunkSize: params.ChunkSize, Encrypt: params.Encrypt, RootDirectory: params.RootDirectory, } return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: d, }, }, }, nil } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { content, err := d.Bucket.Get(d.ossPath(path)) if err != nil { return nil, parseError(path, err) } return content, nil } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { return parseError(path, d.Bucket.Put(d.ossPath(path), contents, d.getContentType(), getPermissions(), d.getOptions())) } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { headers := make(http.Header) headers.Add("Range", "bytes="+strconv.FormatInt(offset, 10)+"-") resp, err := d.Bucket.GetResponseWithHeaders(d.ossPath(path), headers) if err != nil { return nil, parseError(path, err) } // Due to Aliyun OSS API, status 200 and whole object will be return instead of an // InvalidRange error when range is invalid. // // OSS sever will always return http.StatusPartialContent if range is acceptable. if resp.StatusCode != http.StatusPartialContent { resp.Body.Close() return ioutil.NopCloser(bytes.NewReader(nil)), nil } return resp.Body, nil } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { key := d.ossPath(path) if !append { // TODO (brianbland): cancel other uploads at this path multi, err := d.Bucket.InitMulti(key, d.getContentType(), getPermissions(), d.getOptions()) if err != nil { return nil, err } return d.newWriter(key, multi, nil), nil } multis, _, err := d.Bucket.ListMulti(key, "") if err != nil { return nil, parseError(path, err) } for _, multi := range multis { if key != multi.Key { continue } parts, err := multi.ListParts() if err != nil { return nil, parseError(path, err) } var multiSize int64 for _, part := range parts { multiSize += part.Size } return d.newWriter(key, multi, parts), nil } return nil, storagedriver.PathNotFoundError{Path: path} } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { listResponse, err := d.Bucket.List(d.ossPath(path), "", "", 1) if err != nil { return nil, err } fi := storagedriver.FileInfoFields{ Path: path, } if len(listResponse.Contents) == 1 { if listResponse.Contents[0].Key != d.ossPath(path) { fi.IsDir = true } else { fi.IsDir = false fi.Size = listResponse.Contents[0].Size timestamp, err := time.Parse(time.RFC3339Nano, listResponse.Contents[0].LastModified) if err != nil { return nil, err } fi.ModTime = timestamp } } else if len(listResponse.CommonPrefixes) == 1 { fi.IsDir = true } else { return nil, storagedriver.PathNotFoundError{Path: path} } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } // List returns a list of the objects that are direct descendants of the given path. func (d *driver) List(ctx context.Context, opath string) ([]string, error) { path := opath if path != "/" && opath[len(path)-1] != '/' { path = path + "/" } // This is to cover for the cases when the rootDirectory of the driver is either "" or "/". // In those cases, there is no root prefix to replace and we must actually add a "/" to all // results in order to keep them as valid paths as recognized by storagedriver.PathRegexp prefix := "" if d.ossPath("") == "" { prefix = "/" } listResponse, err := d.Bucket.List(d.ossPath(path), "/", "", listMax) if err != nil { return nil, parseError(opath, err) } files := []string{} directories := []string{} for { for _, key := range listResponse.Contents { files = append(files, strings.Replace(key.Key, d.ossPath(""), prefix, 1)) } for _, commonPrefix := range listResponse.CommonPrefixes { directories = append(directories, strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.ossPath(""), prefix, 1)) } if listResponse.IsTruncated { listResponse, err = d.Bucket.List(d.ossPath(path), "/", listResponse.NextMarker, listMax) if err != nil { return nil, err } } else { break } } if opath != "/" { if len(files) == 0 && len(directories) == 0 { // Treat empty response as missing directory, since we don't actually // have directories in s3. return nil, storagedriver.PathNotFoundError{Path: opath} } } return append(files, directories...), nil } const maxConcurrency = 10 // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { logrus.Infof("Move from %s to %s", d.ossPath(sourcePath), d.ossPath(destPath)) err := d.Bucket.CopyLargeFileInParallel(d.ossPath(sourcePath), d.ossPath(destPath), d.getContentType(), getPermissions(), oss.Options{}, maxConcurrency) if err != nil { logrus.Errorf("Failed for move from %s to %s: %v", d.ossPath(sourcePath), d.ossPath(destPath), err) return parseError(sourcePath, err) } return d.Delete(ctx, sourcePath) } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, path string) error { ossPath := d.ossPath(path) listResponse, err := d.Bucket.List(ossPath, "", "", listMax) if err != nil || len(listResponse.Contents) == 0 { return storagedriver.PathNotFoundError{Path: path} } ossObjects := make([]oss.Object, listMax) for len(listResponse.Contents) > 0 { numOssObjects := len(listResponse.Contents) for index, key := range listResponse.Contents { // Stop if we encounter a key that is not a subpath (so that deleting "/a" does not delete "/ab"). if len(key.Key) > len(ossPath) && (key.Key)[len(ossPath)] != '/' { numOssObjects = index break } ossObjects[index].Key = key.Key } err := d.Bucket.DelMulti(oss.Delete{Quiet: false, Objects: ossObjects[0:numOssObjects]}) if err != nil { return nil } if numOssObjects < len(listResponse.Contents) { return nil } listResponse, err = d.Bucket.List(d.ossPath(path), "", "", listMax) if err != nil { return err } } return nil } // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { methodString := "GET" method, ok := options["method"] if ok { methodString, ok = method.(string) if !ok || (methodString != "GET") { return "", storagedriver.ErrUnsupportedMethod{} } } expiresTime := time.Now().Add(20 * time.Minute) expires, ok := options["expiry"] if ok { et, ok := expires.(time.Time) if ok { expiresTime = et } } logrus.Infof("methodString: %s, expiresTime: %v", methodString, expiresTime) signedURL := d.Bucket.SignedURLWithMethod(methodString, d.ossPath(path), expiresTime, nil, nil) logrus.Infof("signed URL: %s", signedURL) return signedURL, nil } func (d *driver) ossPath(path string) string { return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/") } func parseError(path string, err error) error { if ossErr, ok := err.(*oss.Error); ok && ossErr.StatusCode == http.StatusNotFound && (ossErr.Code == "NoSuchKey" || ossErr.Code == "") { return storagedriver.PathNotFoundError{Path: path} } return err } func hasCode(err error, code string) bool { ossErr, ok := err.(*oss.Error) return ok && ossErr.Code == code } func (d *driver) getOptions() oss.Options { return oss.Options{ServerSideEncryption: d.Encrypt} } func getPermissions() oss.ACL { return oss.Private } func (d *driver) getContentType() string { return "application/octet-stream" } // writer attempts to upload parts to S3 in a buffered fashion where the last // part is at least as large as the chunksize, so the multipart upload could be // cleanly resumed in the future. This is violated if Close is called after less // than a full chunk is written. type writer struct { driver *driver key string multi *oss.Multi parts []oss.Part size int64 readyPart []byte pendingPart []byte closed bool committed bool cancelled bool } func (d *driver) newWriter(key string, multi *oss.Multi, parts []oss.Part) storagedriver.FileWriter { var size int64 for _, part := range parts { size += part.Size } return &writer{ driver: d, key: key, multi: multi, parts: parts, size: size, } } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } // If the last written part is smaller than minChunkSize, we need to make a // new multipart upload :sadface: if len(w.parts) > 0 && int(w.parts[len(w.parts)-1].Size) < minChunkSize { err := w.multi.Complete(w.parts) if err != nil { w.multi.Abort() return 0, err } multi, err := w.driver.Bucket.InitMulti(w.key, w.driver.getContentType(), getPermissions(), w.driver.getOptions()) if err != nil { return 0, err } w.multi = multi // If the entire written file is smaller than minChunkSize, we need to make // a new part from scratch :double sad face: if w.size < minChunkSize { contents, err := w.driver.Bucket.Get(w.key) if err != nil { return 0, err } w.parts = nil w.readyPart = contents } else { // Otherwise we can use the old file as the new first part _, part, err := multi.PutPartCopy(1, oss.CopyOptions{}, w.driver.Bucket.Name+"/"+w.key) if err != nil { return 0, err } w.parts = []oss.Part{part} } } var n int for len(p) > 0 { // If no parts are ready to write, fill up the first part if neededBytes := int(w.driver.ChunkSize) - len(w.readyPart); neededBytes > 0 { if len(p) >= neededBytes { w.readyPart = append(w.readyPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] } else { w.readyPart = append(w.readyPart, p...) n += len(p) p = nil } } if neededBytes := int(w.driver.ChunkSize) - len(w.pendingPart); neededBytes > 0 { if len(p) >= neededBytes { w.pendingPart = append(w.pendingPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] err := w.flushPart() if err != nil { w.size += int64(n) return n, err } } else { w.pendingPart = append(w.pendingPart, p...) n += len(p) p = nil } } } w.size += int64(n) return n, nil } func (w *writer) Size() int64 { return w.size } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } w.closed = true return w.flushPart() } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true err := w.multi.Abort() return err } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } err := w.flushPart() if err != nil { return err } w.committed = true err = w.multi.Complete(w.parts) if err != nil { w.multi.Abort() return err } return nil } // flushPart flushes buffers to write a part to S3. // Only called by Write (with both buffers full) and Close/Commit (always) func (w *writer) flushPart() error { if len(w.readyPart) == 0 && len(w.pendingPart) == 0 { // nothing to write return nil } if len(w.pendingPart) < int(w.driver.ChunkSize) { // closing with a small pending part // combine ready and pending to avoid writing a small part w.readyPart = append(w.readyPart, w.pendingPart...) w.pendingPart = nil } part, err := w.multi.PutPart(len(w.parts)+1, bytes.NewReader(w.readyPart)) if err != nil { return err } w.parts = append(w.parts, part) w.readyPart = w.pendingPart w.pendingPart = nil return nil } docker-registry-2.6.2~ds1/registry/storage/driver/oss/oss_test.go000066400000000000000000000071761313450123100252550ustar00rootroot00000000000000// +build include_oss package oss import ( "io/ioutil" alioss "github.com/denverdino/aliyungo/oss" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" //"log" "os" "strconv" "testing" "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } var ossDriverConstructor func(rootDirectory string) (*Driver, error) var skipCheck func() string func init() { accessKey := os.Getenv("ALIYUN_ACCESS_KEY_ID") secretKey := os.Getenv("ALIYUN_ACCESS_KEY_SECRET") bucket := os.Getenv("OSS_BUCKET") region := os.Getenv("OSS_REGION") internal := os.Getenv("OSS_INTERNAL") encrypt := os.Getenv("OSS_ENCRYPT") secure := os.Getenv("OSS_SECURE") endpoint := os.Getenv("OSS_ENDPOINT") root, err := ioutil.TempDir("", "driver-") if err != nil { panic(err) } defer os.Remove(root) ossDriverConstructor = func(rootDirectory string) (*Driver, error) { encryptBool := false if encrypt != "" { encryptBool, err = strconv.ParseBool(encrypt) if err != nil { return nil, err } } secureBool := false if secure != "" { secureBool, err = strconv.ParseBool(secure) if err != nil { return nil, err } } internalBool := false if internal != "" { internalBool, err = strconv.ParseBool(internal) if err != nil { return nil, err } } parameters := DriverParameters{ AccessKeyID: accessKey, AccessKeySecret: secretKey, Bucket: bucket, Region: alioss.Region(region), Internal: internalBool, ChunkSize: minChunkSize, RootDirectory: rootDirectory, Encrypt: encryptBool, Secure: secureBool, Endpoint: endpoint, } return New(parameters) } // Skip OSS storage driver tests if environment variable parameters are not provided skipCheck = func() string { if accessKey == "" || secretKey == "" || region == "" || bucket == "" || encrypt == "" { return "Must set ALIYUN_ACCESS_KEY_ID, ALIYUN_ACCESS_KEY_SECRET, OSS_REGION, OSS_BUCKET, and OSS_ENCRYPT to run OSS tests" } return "" } testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) { return ossDriverConstructor(root) }, skipCheck) } func TestEmptyRootList(t *testing.T) { if skipCheck() != "" { t.Skip(skipCheck()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) rootedDriver, err := ossDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } emptyRootDriver, err := ossDriverConstructor("") if err != nil { t.Fatalf("unexpected error creating empty root driver: %v", err) } slashRootDriver, err := ossDriverConstructor("/") if err != nil { t.Fatalf("unexpected error creating slash root driver: %v", err) } filename := "/test" contents := []byte("contents") ctx := context.Background() err = rootedDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer rootedDriver.Delete(ctx, filename) keys, err := emptyRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } keys, err = slashRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } } docker-registry-2.6.2~ds1/registry/storage/driver/s3-aws/000077500000000000000000000000001313450123100233615ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/s3-aws/s3.go000066400000000000000000001033151313450123100242400ustar00rootroot00000000000000// Package s3 provides a storagedriver.StorageDriver implementation to // store blobs in Amazon S3 cloud storage. // // This package leverages the official aws client library for interfacing with // S3. // // Because S3 is a key, value store the Stat call does not support last modification // time for directories (directories are an abstraction for key, value stores) // // Keep in mind that S3 guarantees only read-after-write consistency for new // objects, but no read-after-update or list-after-write consistency. package s3 import ( "bytes" "fmt" "io" "io/ioutil" "math" "net/http" "reflect" "sort" "strconv" "strings" "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/client/transport" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const driverName = "s3aws" // minChunkSize defines the minimum multipart upload chunk size // S3 API requires multipart upload chunks to be at least 5MB const minChunkSize = 5 << 20 // maxChunkSize defines the maximum multipart upload chunk size allowed by S3. const maxChunkSize = 5 << 30 const defaultChunkSize = 2 * minChunkSize const ( // defaultMultipartCopyChunkSize defines the default chunk size for all // but the last Upload Part - Copy operation of a multipart copy. // Empirically, 32 MB is optimal. defaultMultipartCopyChunkSize = 32 << 20 // defaultMultipartCopyMaxConcurrency defines the default maximum number // of concurrent Upload Part - Copy operations for a multipart copy. defaultMultipartCopyMaxConcurrency = 100 // defaultMultipartCopyThresholdSize defines the default object size // above which multipart copy will be used. (PUT Object - Copy is used // for objects at or below this size.) Empirically, 32 MB is optimal. defaultMultipartCopyThresholdSize = 32 << 20 ) // listMax is the largest amount of objects you can request from S3 in a list call const listMax = 1000 // noStorageClass defines the value to be used if storage class is not supported by the S3 endpoint const noStorageClass = "NONE" // validRegions maps known s3 region identifiers to region descriptors var validRegions = map[string]struct{}{} // validObjectACLs contains known s3 object Acls var validObjectACLs = map[string]struct{}{} //DriverParameters A struct that encapsulates all of the driver parameters after all values have been set type DriverParameters struct { AccessKey string SecretKey string Bucket string Region string RegionEndpoint string Encrypt bool KeyID string Secure bool V4Auth bool ChunkSize int64 MultipartCopyChunkSize int64 MultipartCopyMaxConcurrency int64 MultipartCopyThresholdSize int64 RootDirectory string StorageClass string UserAgent string ObjectACL string } func init() { for _, region := range []string{ "us-east-1", "us-east-2", "us-west-1", "us-west-2", "eu-west-1", "eu-central-1", "ap-southeast-1", "ap-southeast-2", "ap-northeast-1", "ap-northeast-2", "sa-east-1", "cn-north-1", "us-gov-west-1", } { validRegions[region] = struct{}{} } for _, objectACL := range []string{ s3.ObjectCannedACLPrivate, s3.ObjectCannedACLPublicRead, s3.ObjectCannedACLPublicReadWrite, s3.ObjectCannedACLAuthenticatedRead, s3.ObjectCannedACLAwsExecRead, s3.ObjectCannedACLBucketOwnerRead, s3.ObjectCannedACLBucketOwnerFullControl, } { validObjectACLs[objectACL] = struct{}{} } // Register this as the default s3 driver in addition to s3aws factory.Register("s3", &s3DriverFactory{}) factory.Register(driverName, &s3DriverFactory{}) } // s3DriverFactory implements the factory.StorageDriverFactory interface type s3DriverFactory struct{} func (factory *s3DriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } type driver struct { S3 *s3.S3 Bucket string ChunkSize int64 Encrypt bool KeyID string MultipartCopyChunkSize int64 MultipartCopyMaxConcurrency int64 MultipartCopyThresholdSize int64 RootDirectory string StorageClass string ObjectACL string } type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by Amazon S3 // Objects are stored at absolute keys in the provided bucket. type Driver struct { baseEmbed } // FromParameters constructs a new Driver with a given parameters map // Required parameters: // - accesskey // - secretkey // - region // - bucket // - encrypt func FromParameters(parameters map[string]interface{}) (*Driver, error) { // Providing no values for these is valid in case the user is authenticating // with an IAM on an ec2 instance (in which case the instance credentials will // be summoned when GetAuth is called) accessKey := parameters["accesskey"] if accessKey == nil { accessKey = "" } secretKey := parameters["secretkey"] if secretKey == nil { secretKey = "" } regionEndpoint := parameters["regionendpoint"] if regionEndpoint == nil { regionEndpoint = "" } regionName, ok := parameters["region"] if regionName == nil || fmt.Sprint(regionName) == "" { return nil, fmt.Errorf("No region parameter provided") } region := fmt.Sprint(regionName) // Don't check the region value if a custom endpoint is provided. if regionEndpoint == "" { if _, ok = validRegions[region]; !ok { return nil, fmt.Errorf("Invalid region provided: %v", region) } } bucket := parameters["bucket"] if bucket == nil || fmt.Sprint(bucket) == "" { return nil, fmt.Errorf("No bucket parameter provided") } encryptBool := false encrypt := parameters["encrypt"] switch encrypt := encrypt.(type) { case string: b, err := strconv.ParseBool(encrypt) if err != nil { return nil, fmt.Errorf("The encrypt parameter should be a boolean") } encryptBool = b case bool: encryptBool = encrypt case nil: // do nothing default: return nil, fmt.Errorf("The encrypt parameter should be a boolean") } secureBool := true secure := parameters["secure"] switch secure := secure.(type) { case string: b, err := strconv.ParseBool(secure) if err != nil { return nil, fmt.Errorf("The secure parameter should be a boolean") } secureBool = b case bool: secureBool = secure case nil: // do nothing default: return nil, fmt.Errorf("The secure parameter should be a boolean") } v4Bool := true v4auth := parameters["v4auth"] switch v4auth := v4auth.(type) { case string: b, err := strconv.ParseBool(v4auth) if err != nil { return nil, fmt.Errorf("The v4auth parameter should be a boolean") } v4Bool = b case bool: v4Bool = v4auth case nil: // do nothing default: return nil, fmt.Errorf("The v4auth parameter should be a boolean") } keyID := parameters["keyid"] if keyID == nil { keyID = "" } chunkSize, err := getParameterAsInt64(parameters, "chunksize", defaultChunkSize, minChunkSize, maxChunkSize) if err != nil { return nil, err } multipartCopyChunkSize, err := getParameterAsInt64(parameters, "multipartcopychunksize", defaultMultipartCopyChunkSize, minChunkSize, maxChunkSize) if err != nil { return nil, err } multipartCopyMaxConcurrency, err := getParameterAsInt64(parameters, "multipartcopymaxconcurrency", defaultMultipartCopyMaxConcurrency, 1, math.MaxInt64) if err != nil { return nil, err } multipartCopyThresholdSize, err := getParameterAsInt64(parameters, "multipartcopythresholdsize", defaultMultipartCopyThresholdSize, 0, maxChunkSize) if err != nil { return nil, err } rootDirectory := parameters["rootdirectory"] if rootDirectory == nil { rootDirectory = "" } storageClass := s3.StorageClassStandard storageClassParam := parameters["storageclass"] if storageClassParam != nil { storageClassString, ok := storageClassParam.(string) if !ok { return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []string{s3.StorageClassStandard, s3.StorageClassReducedRedundancy}, storageClassParam) } // All valid storage class parameters are UPPERCASE, so be a bit more flexible here storageClassString = strings.ToUpper(storageClassString) if storageClassString != noStorageClass && storageClassString != s3.StorageClassStandard && storageClassString != s3.StorageClassReducedRedundancy { return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []string{noStorageClass, s3.StorageClassStandard, s3.StorageClassReducedRedundancy}, storageClassParam) } storageClass = storageClassString } userAgent := parameters["useragent"] if userAgent == nil { userAgent = "" } objectACL := s3.ObjectCannedACLPrivate objectACLParam := parameters["objectacl"] if objectACLParam != nil { objectACLString, ok := objectACLParam.(string) if !ok { return nil, fmt.Errorf("Invalid value for objectacl parameter: %v", objectACLParam) } if _, ok = validObjectACLs[objectACLString]; !ok { return nil, fmt.Errorf("Invalid value for objectacl parameter: %v", objectACLParam) } objectACL = objectACLString } params := DriverParameters{ fmt.Sprint(accessKey), fmt.Sprint(secretKey), fmt.Sprint(bucket), region, fmt.Sprint(regionEndpoint), encryptBool, fmt.Sprint(keyID), secureBool, v4Bool, chunkSize, multipartCopyChunkSize, multipartCopyMaxConcurrency, multipartCopyThresholdSize, fmt.Sprint(rootDirectory), storageClass, fmt.Sprint(userAgent), objectACL, } return New(params) } // getParameterAsInt64 converts paramaters[name] to an int64 value (using // defaultt if nil), verifies it is no smaller than min, and returns it. func getParameterAsInt64(parameters map[string]interface{}, name string, defaultt int64, min int64, max int64) (int64, error) { rv := defaultt param := parameters[name] switch v := param.(type) { case string: vv, err := strconv.ParseInt(v, 0, 64) if err != nil { return 0, fmt.Errorf("%s parameter must be an integer, %v invalid", name, param) } rv = vv case int64: rv = v case int, uint, int32, uint32, uint64: rv = reflect.ValueOf(v).Convert(reflect.TypeOf(rv)).Int() case nil: // do nothing default: return 0, fmt.Errorf("invalid value for %s: %#v", name, param) } if rv < min || rv > max { return 0, fmt.Errorf("The %s %#v parameter should be a number between %d and %d (inclusive)", name, rv, min, max) } return rv, nil } // New constructs a new Driver with the given AWS credentials, region, encryption flag, and // bucketName func New(params DriverParameters) (*Driver, error) { if !params.V4Auth && (params.RegionEndpoint == "" || strings.Contains(params.RegionEndpoint, "s3.amazonaws.com")) { return nil, fmt.Errorf("On Amazon S3 this storage driver can only be used with v4 authentication") } awsConfig := aws.NewConfig() creds := credentials.NewChainCredentials([]credentials.Provider{ &credentials.StaticProvider{ Value: credentials.Value{ AccessKeyID: params.AccessKey, SecretAccessKey: params.SecretKey, }, }, &credentials.EnvProvider{}, &credentials.SharedCredentialsProvider{}, &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(session.New())}, }) if params.RegionEndpoint != "" { awsConfig.WithS3ForcePathStyle(true) awsConfig.WithEndpoint(params.RegionEndpoint) } awsConfig.WithCredentials(creds) awsConfig.WithRegion(params.Region) awsConfig.WithDisableSSL(!params.Secure) if params.UserAgent != "" { awsConfig.WithHTTPClient(&http.Client{ Transport: transport.NewTransport(http.DefaultTransport, transport.NewHeaderRequestModifier(http.Header{http.CanonicalHeaderKey("User-Agent"): []string{params.UserAgent}})), }) } s3obj := s3.New(session.New(awsConfig)) // enable S3 compatible signature v2 signing instead if !params.V4Auth { setv2Handlers(s3obj) } // TODO Currently multipart uploads have no timestamps, so this would be unwise // if you initiated a new s3driver while another one is running on the same bucket. // multis, _, err := bucket.ListMulti("", "") // if err != nil { // return nil, err // } // for _, multi := range multis { // err := multi.Abort() // //TODO appropriate to do this error checking? // if err != nil { // return nil, err // } // } d := &driver{ S3: s3obj, Bucket: params.Bucket, ChunkSize: params.ChunkSize, Encrypt: params.Encrypt, KeyID: params.KeyID, MultipartCopyChunkSize: params.MultipartCopyChunkSize, MultipartCopyMaxConcurrency: params.MultipartCopyMaxConcurrency, MultipartCopyThresholdSize: params.MultipartCopyThresholdSize, RootDirectory: params.RootDirectory, StorageClass: params.StorageClass, ObjectACL: params.ObjectACL, } return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: d, }, }, }, nil } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { reader, err := d.Reader(ctx, path, 0) if err != nil { return nil, err } return ioutil.ReadAll(reader) } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { _, err := d.S3.PutObject(&s3.PutObjectInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(path)), ContentType: d.getContentType(), ACL: d.getACL(), ServerSideEncryption: d.getEncryptionMode(), SSEKMSKeyId: d.getSSEKMSKeyID(), StorageClass: d.getStorageClass(), Body: bytes.NewReader(contents), }) return parseError(path, err) } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { resp, err := d.S3.GetObject(&s3.GetObjectInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(path)), Range: aws.String("bytes=" + strconv.FormatInt(offset, 10) + "-"), }) if err != nil { if s3Err, ok := err.(awserr.Error); ok && s3Err.Code() == "InvalidRange" { return ioutil.NopCloser(bytes.NewReader(nil)), nil } return nil, parseError(path, err) } return resp.Body, nil } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { key := d.s3Path(path) if !append { // TODO (brianbland): cancel other uploads at this path resp, err := d.S3.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ Bucket: aws.String(d.Bucket), Key: aws.String(key), ContentType: d.getContentType(), ACL: d.getACL(), ServerSideEncryption: d.getEncryptionMode(), SSEKMSKeyId: d.getSSEKMSKeyID(), StorageClass: d.getStorageClass(), }) if err != nil { return nil, err } return d.newWriter(key, *resp.UploadId, nil), nil } resp, err := d.S3.ListMultipartUploads(&s3.ListMultipartUploadsInput{ Bucket: aws.String(d.Bucket), Prefix: aws.String(key), }) if err != nil { return nil, parseError(path, err) } for _, multi := range resp.Uploads { if key != *multi.Key { continue } resp, err := d.S3.ListParts(&s3.ListPartsInput{ Bucket: aws.String(d.Bucket), Key: aws.String(key), UploadId: multi.UploadId, }) if err != nil { return nil, parseError(path, err) } var multiSize int64 for _, part := range resp.Parts { multiSize += *part.Size } return d.newWriter(key, *multi.UploadId, resp.Parts), nil } return nil, storagedriver.PathNotFoundError{Path: path} } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { resp, err := d.S3.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(d.Bucket), Prefix: aws.String(d.s3Path(path)), MaxKeys: aws.Int64(1), }) if err != nil { return nil, err } fi := storagedriver.FileInfoFields{ Path: path, } if len(resp.Contents) == 1 { if *resp.Contents[0].Key != d.s3Path(path) { fi.IsDir = true } else { fi.IsDir = false fi.Size = *resp.Contents[0].Size fi.ModTime = *resp.Contents[0].LastModified } } else if len(resp.CommonPrefixes) == 1 { fi.IsDir = true } else { return nil, storagedriver.PathNotFoundError{Path: path} } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } // List returns a list of the objects that are direct descendants of the given path. func (d *driver) List(ctx context.Context, opath string) ([]string, error) { path := opath if path != "/" && path[len(path)-1] != '/' { path = path + "/" } // This is to cover for the cases when the rootDirectory of the driver is either "" or "/". // In those cases, there is no root prefix to replace and we must actually add a "/" to all // results in order to keep them as valid paths as recognized by storagedriver.PathRegexp prefix := "" if d.s3Path("") == "" { prefix = "/" } resp, err := d.S3.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(d.Bucket), Prefix: aws.String(d.s3Path(path)), Delimiter: aws.String("/"), MaxKeys: aws.Int64(listMax), }) if err != nil { return nil, parseError(opath, err) } files := []string{} directories := []string{} for { for _, key := range resp.Contents { files = append(files, strings.Replace(*key.Key, d.s3Path(""), prefix, 1)) } for _, commonPrefix := range resp.CommonPrefixes { commonPrefix := *commonPrefix.Prefix directories = append(directories, strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.s3Path(""), prefix, 1)) } if *resp.IsTruncated { resp, err = d.S3.ListObjects(&s3.ListObjectsInput{ Bucket: aws.String(d.Bucket), Prefix: aws.String(d.s3Path(path)), Delimiter: aws.String("/"), MaxKeys: aws.Int64(listMax), Marker: resp.NextMarker, }) if err != nil { return nil, err } } else { break } } if opath != "/" { if len(files) == 0 && len(directories) == 0 { // Treat empty response as missing directory, since we don't actually // have directories in s3. return nil, storagedriver.PathNotFoundError{Path: opath} } } return append(files, directories...), nil } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { /* This is terrible, but aws doesn't have an actual move. */ if err := d.copy(ctx, sourcePath, destPath); err != nil { return err } return d.Delete(ctx, sourcePath) } // copy copies an object stored at sourcePath to destPath. func (d *driver) copy(ctx context.Context, sourcePath string, destPath string) error { // S3 can copy objects up to 5 GB in size with a single PUT Object - Copy // operation. For larger objects, the multipart upload API must be used. // // Empirically, multipart copy is fastest with 32 MB parts and is faster // than PUT Object - Copy for objects larger than 32 MB. fileInfo, err := d.Stat(ctx, sourcePath) if err != nil { return parseError(sourcePath, err) } if fileInfo.Size() <= d.MultipartCopyThresholdSize { _, err := d.S3.CopyObject(&s3.CopyObjectInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(destPath)), ContentType: d.getContentType(), ACL: d.getACL(), ServerSideEncryption: d.getEncryptionMode(), SSEKMSKeyId: d.getSSEKMSKeyID(), StorageClass: d.getStorageClass(), CopySource: aws.String(d.Bucket + "/" + d.s3Path(sourcePath)), }) if err != nil { return parseError(sourcePath, err) } return nil } // Even in the worst case, a multipart copy should take no more // than a few minutes, so 30 minutes is very conservative. expires := time.Now().Add(time.Duration(30) * time.Minute) createResp, err := d.S3.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(destPath)), ContentType: d.getContentType(), ACL: d.getACL(), Expires: aws.Time(expires), SSEKMSKeyId: d.getSSEKMSKeyID(), ServerSideEncryption: d.getEncryptionMode(), StorageClass: d.getStorageClass(), }) if err != nil { return err } numParts := (fileInfo.Size() + d.MultipartCopyChunkSize - 1) / d.MultipartCopyChunkSize completedParts := make([]*s3.CompletedPart, numParts) errChan := make(chan error, numParts) limiter := make(chan struct{}, d.MultipartCopyMaxConcurrency) for i := range completedParts { i := int64(i) go func() { limiter <- struct{}{} firstByte := i * d.MultipartCopyChunkSize lastByte := firstByte + d.MultipartCopyChunkSize - 1 if lastByte >= fileInfo.Size() { lastByte = fileInfo.Size() - 1 } uploadResp, err := d.S3.UploadPartCopy(&s3.UploadPartCopyInput{ Bucket: aws.String(d.Bucket), CopySource: aws.String(d.Bucket + "/" + d.s3Path(sourcePath)), Key: aws.String(d.s3Path(destPath)), PartNumber: aws.Int64(i + 1), UploadId: createResp.UploadId, CopySourceRange: aws.String(fmt.Sprintf("bytes=%d-%d", firstByte, lastByte)), }) if err == nil { completedParts[i] = &s3.CompletedPart{ ETag: uploadResp.CopyPartResult.ETag, PartNumber: aws.Int64(i + 1), } } errChan <- err <-limiter }() } for range completedParts { err := <-errChan if err != nil { return err } } _, err = d.S3.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(destPath)), UploadId: createResp.UploadId, MultipartUpload: &s3.CompletedMultipartUpload{Parts: completedParts}, }) return err } func min(a, b int) int { if a < b { return a } return b } // Delete recursively deletes all objects stored at "path" and its subpaths. // We must be careful since S3 does not guarantee read after delete consistency func (d *driver) Delete(ctx context.Context, path string) error { s3Objects := make([]*s3.ObjectIdentifier, 0, listMax) s3Path := d.s3Path(path) listObjectsInput := &s3.ListObjectsInput{ Bucket: aws.String(d.Bucket), Prefix: aws.String(s3Path), } ListLoop: for { // list all the objects resp, err := d.S3.ListObjects(listObjectsInput) // resp.Contents can only be empty on the first call // if there were no more results to return after the first call, resp.IsTruncated would have been false // and the loop would be exited without recalling ListObjects if err != nil || len(resp.Contents) == 0 { return storagedriver.PathNotFoundError{Path: path} } for _, key := range resp.Contents { // Stop if we encounter a key that is not a subpath (so that deleting "/a" does not delete "/ab"). if len(*key.Key) > len(s3Path) && (*key.Key)[len(s3Path)] != '/' { break ListLoop } s3Objects = append(s3Objects, &s3.ObjectIdentifier{ Key: key.Key, }) } // resp.Contents must have at least one element or we would have returned not found listObjectsInput.Marker = resp.Contents[len(resp.Contents)-1].Key // from the s3 api docs, IsTruncated "specifies whether (true) or not (false) all of the results were returned" // if everything has been returned, break if resp.IsTruncated == nil || !*resp.IsTruncated { break } } // need to chunk objects into groups of 1000 per s3 restrictions total := len(s3Objects) for i := 0; i < total; i += 1000 { _, err := d.S3.DeleteObjects(&s3.DeleteObjectsInput{ Bucket: aws.String(d.Bucket), Delete: &s3.Delete{ Objects: s3Objects[i:min(i+1000, total)], Quiet: aws.Bool(false), }, }) if err != nil { return err } } return nil } // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { methodString := "GET" method, ok := options["method"] if ok { methodString, ok = method.(string) if !ok || (methodString != "GET" && methodString != "HEAD") { return "", storagedriver.ErrUnsupportedMethod{} } } expiresIn := 20 * time.Minute expires, ok := options["expiry"] if ok { et, ok := expires.(time.Time) if ok { expiresIn = et.Sub(time.Now()) } } var req *request.Request switch methodString { case "GET": req, _ = d.S3.GetObjectRequest(&s3.GetObjectInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(path)), }) case "HEAD": req, _ = d.S3.HeadObjectRequest(&s3.HeadObjectInput{ Bucket: aws.String(d.Bucket), Key: aws.String(d.s3Path(path)), }) default: panic("unreachable") } return req.Presign(expiresIn) } func (d *driver) s3Path(path string) string { return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/") } // S3BucketKey returns the s3 bucket key for the given storage driver path. func (d *Driver) S3BucketKey(path string) string { return d.StorageDriver.(*driver).s3Path(path) } func parseError(path string, err error) error { if s3Err, ok := err.(awserr.Error); ok && s3Err.Code() == "NoSuchKey" { return storagedriver.PathNotFoundError{Path: path} } return err } func (d *driver) getEncryptionMode() *string { if !d.Encrypt { return nil } if d.KeyID == "" { return aws.String("AES256") } return aws.String("aws:kms") } func (d *driver) getSSEKMSKeyID() *string { if d.KeyID != "" { return aws.String(d.KeyID) } return nil } func (d *driver) getContentType() *string { return aws.String("application/octet-stream") } func (d *driver) getACL() *string { return aws.String(d.ObjectACL) } func (d *driver) getStorageClass() *string { if d.StorageClass == noStorageClass { return nil } return aws.String(d.StorageClass) } // writer attempts to upload parts to S3 in a buffered fashion where the last // part is at least as large as the chunksize, so the multipart upload could be // cleanly resumed in the future. This is violated if Close is called after less // than a full chunk is written. type writer struct { driver *driver key string uploadID string parts []*s3.Part size int64 readyPart []byte pendingPart []byte closed bool committed bool cancelled bool } func (d *driver) newWriter(key, uploadID string, parts []*s3.Part) storagedriver.FileWriter { var size int64 for _, part := range parts { size += *part.Size } return &writer{ driver: d, key: key, uploadID: uploadID, parts: parts, size: size, } } type completedParts []*s3.CompletedPart func (a completedParts) Len() int { return len(a) } func (a completedParts) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a completedParts) Less(i, j int) bool { return *a[i].PartNumber < *a[j].PartNumber } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } // If the last written part is smaller than minChunkSize, we need to make a // new multipart upload :sadface: if len(w.parts) > 0 && int(*w.parts[len(w.parts)-1].Size) < minChunkSize { var completedUploadedParts completedParts for _, part := range w.parts { completedUploadedParts = append(completedUploadedParts, &s3.CompletedPart{ ETag: part.ETag, PartNumber: part.PartNumber, }) } sort.Sort(completedUploadedParts) _, err := w.driver.S3.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), MultipartUpload: &s3.CompletedMultipartUpload{ Parts: completedUploadedParts, }, }) if err != nil { w.driver.S3.AbortMultipartUpload(&s3.AbortMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), }) return 0, err } resp, err := w.driver.S3.CreateMultipartUpload(&s3.CreateMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), ContentType: w.driver.getContentType(), ACL: w.driver.getACL(), ServerSideEncryption: w.driver.getEncryptionMode(), StorageClass: w.driver.getStorageClass(), }) if err != nil { return 0, err } w.uploadID = *resp.UploadId // If the entire written file is smaller than minChunkSize, we need to make // a new part from scratch :double sad face: if w.size < minChunkSize { resp, err := w.driver.S3.GetObject(&s3.GetObjectInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), }) defer resp.Body.Close() if err != nil { return 0, err } w.parts = nil w.readyPart, err = ioutil.ReadAll(resp.Body) if err != nil { return 0, err } } else { // Otherwise we can use the old file as the new first part copyPartResp, err := w.driver.S3.UploadPartCopy(&s3.UploadPartCopyInput{ Bucket: aws.String(w.driver.Bucket), CopySource: aws.String(w.driver.Bucket + "/" + w.key), Key: aws.String(w.key), PartNumber: aws.Int64(1), UploadId: resp.UploadId, }) if err != nil { return 0, err } w.parts = []*s3.Part{ { ETag: copyPartResp.CopyPartResult.ETag, PartNumber: aws.Int64(1), Size: aws.Int64(w.size), }, } } } var n int for len(p) > 0 { // If no parts are ready to write, fill up the first part if neededBytes := int(w.driver.ChunkSize) - len(w.readyPart); neededBytes > 0 { if len(p) >= neededBytes { w.readyPart = append(w.readyPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] } else { w.readyPart = append(w.readyPart, p...) n += len(p) p = nil } } if neededBytes := int(w.driver.ChunkSize) - len(w.pendingPart); neededBytes > 0 { if len(p) >= neededBytes { w.pendingPart = append(w.pendingPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] err := w.flushPart() if err != nil { w.size += int64(n) return n, err } } else { w.pendingPart = append(w.pendingPart, p...) n += len(p) p = nil } } } w.size += int64(n) return n, nil } func (w *writer) Size() int64 { return w.size } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } w.closed = true return w.flushPart() } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true _, err := w.driver.S3.AbortMultipartUpload(&s3.AbortMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), }) return err } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } err := w.flushPart() if err != nil { return err } w.committed = true var completedUploadedParts completedParts for _, part := range w.parts { completedUploadedParts = append(completedUploadedParts, &s3.CompletedPart{ ETag: part.ETag, PartNumber: part.PartNumber, }) } sort.Sort(completedUploadedParts) _, err = w.driver.S3.CompleteMultipartUpload(&s3.CompleteMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), MultipartUpload: &s3.CompletedMultipartUpload{ Parts: completedUploadedParts, }, }) if err != nil { w.driver.S3.AbortMultipartUpload(&s3.AbortMultipartUploadInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), UploadId: aws.String(w.uploadID), }) return err } return nil } // flushPart flushes buffers to write a part to S3. // Only called by Write (with both buffers full) and Close/Commit (always) func (w *writer) flushPart() error { if len(w.readyPart) == 0 && len(w.pendingPart) == 0 { // nothing to write return nil } if len(w.pendingPart) < int(w.driver.ChunkSize) { // closing with a small pending part // combine ready and pending to avoid writing a small part w.readyPart = append(w.readyPart, w.pendingPart...) w.pendingPart = nil } partNumber := aws.Int64(int64(len(w.parts) + 1)) resp, err := w.driver.S3.UploadPart(&s3.UploadPartInput{ Bucket: aws.String(w.driver.Bucket), Key: aws.String(w.key), PartNumber: partNumber, UploadId: aws.String(w.uploadID), Body: bytes.NewReader(w.readyPart), }) if err != nil { return err } w.parts = append(w.parts, &s3.Part{ ETag: resp.ETag, PartNumber: partNumber, Size: aws.Int64(int64(len(w.readyPart))), }) w.readyPart = w.pendingPart w.pendingPart = nil return nil } docker-registry-2.6.2~ds1/registry/storage/driver/s3-aws/s3_test.go000066400000000000000000000210371313450123100252770ustar00rootroot00000000000000package s3 import ( "bytes" "io/ioutil" "math/rand" "os" "strconv" "testing" "gopkg.in/check.v1" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } var s3DriverConstructor func(rootDirectory, storageClass string) (*Driver, error) var skipS3 func() string func init() { accessKey := os.Getenv("AWS_ACCESS_KEY") secretKey := os.Getenv("AWS_SECRET_KEY") bucket := os.Getenv("S3_BUCKET") encrypt := os.Getenv("S3_ENCRYPT") keyID := os.Getenv("S3_KEY_ID") secure := os.Getenv("S3_SECURE") v4Auth := os.Getenv("S3_V4_AUTH") region := os.Getenv("AWS_REGION") objectACL := os.Getenv("S3_OBJECT_ACL") root, err := ioutil.TempDir("", "driver-") regionEndpoint := os.Getenv("REGION_ENDPOINT") if err != nil { panic(err) } defer os.Remove(root) s3DriverConstructor = func(rootDirectory, storageClass string) (*Driver, error) { encryptBool := false if encrypt != "" { encryptBool, err = strconv.ParseBool(encrypt) if err != nil { return nil, err } } secureBool := true if secure != "" { secureBool, err = strconv.ParseBool(secure) if err != nil { return nil, err } } v4Bool := true if v4Auth != "" { v4Bool, err = strconv.ParseBool(v4Auth) if err != nil { return nil, err } } parameters := DriverParameters{ accessKey, secretKey, bucket, region, regionEndpoint, encryptBool, keyID, secureBool, v4Bool, minChunkSize, defaultMultipartCopyChunkSize, defaultMultipartCopyMaxConcurrency, defaultMultipartCopyThresholdSize, rootDirectory, storageClass, driverName + "-test", objectACL, } return New(parameters) } // Skip S3 storage driver tests if environment variable parameters are not provided skipS3 = func() string { if accessKey == "" || secretKey == "" || region == "" || bucket == "" || encrypt == "" { return "Must set AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION, S3_BUCKET, and S3_ENCRYPT to run S3 tests" } return "" } testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) { return s3DriverConstructor(root, s3.StorageClassStandard) }, skipS3) } func TestEmptyRootList(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) rootedDriver, err := s3DriverConstructor(validRoot, s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } emptyRootDriver, err := s3DriverConstructor("", s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating empty root driver: %v", err) } slashRootDriver, err := s3DriverConstructor("/", s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating slash root driver: %v", err) } filename := "/test" contents := []byte("contents") ctx := context.Background() err = rootedDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer rootedDriver.Delete(ctx, filename) keys, err := emptyRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } keys, err = slashRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } } func TestStorageClass(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } rootDir, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(rootDir) standardDriver, err := s3DriverConstructor(rootDir, s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating driver with standard storage: %v", err) } rrDriver, err := s3DriverConstructor(rootDir, s3.StorageClassReducedRedundancy) if err != nil { t.Fatalf("unexpected error creating driver with reduced redundancy storage: %v", err) } if _, err = s3DriverConstructor(rootDir, noStorageClass); err != nil { t.Fatalf("unexpected error creating driver without storage class: %v", err) } standardFilename := "/test-standard" rrFilename := "/test-rr" contents := []byte("contents") ctx := context.Background() err = standardDriver.PutContent(ctx, standardFilename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer standardDriver.Delete(ctx, standardFilename) err = rrDriver.PutContent(ctx, rrFilename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer rrDriver.Delete(ctx, rrFilename) standardDriverUnwrapped := standardDriver.Base.StorageDriver.(*driver) resp, err := standardDriverUnwrapped.S3.GetObject(&s3.GetObjectInput{ Bucket: aws.String(standardDriverUnwrapped.Bucket), Key: aws.String(standardDriverUnwrapped.s3Path(standardFilename)), }) if err != nil { t.Fatalf("unexpected error retrieving standard storage file: %v", err) } defer resp.Body.Close() // Amazon only populates this header value for non-standard storage classes if resp.StorageClass != nil { t.Fatalf("unexpected storage class for standard file: %v", resp.StorageClass) } rrDriverUnwrapped := rrDriver.Base.StorageDriver.(*driver) resp, err = rrDriverUnwrapped.S3.GetObject(&s3.GetObjectInput{ Bucket: aws.String(rrDriverUnwrapped.Bucket), Key: aws.String(rrDriverUnwrapped.s3Path(rrFilename)), }) if err != nil { t.Fatalf("unexpected error retrieving reduced-redundancy storage file: %v", err) } defer resp.Body.Close() if resp.StorageClass == nil { t.Fatalf("unexpected storage class for reduced-redundancy file: %v", s3.StorageClassStandard) } else if *resp.StorageClass != s3.StorageClassReducedRedundancy { t.Fatalf("unexpected storage class for reduced-redundancy file: %v", *resp.StorageClass) } } func TestOverThousandBlobs(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } rootDir, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(rootDir) standardDriver, err := s3DriverConstructor(rootDir, s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating driver with standard storage: %v", err) } ctx := context.Background() for i := 0; i < 1005; i++ { filename := "/thousandfiletest/file" + strconv.Itoa(i) contents := []byte("contents") err = standardDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } } // cant actually verify deletion because read-after-delete is inconsistent, but can ensure no errors err = standardDriver.Delete(ctx, "/thousandfiletest") if err != nil { t.Fatalf("unexpected error deleting thousand files: %v", err) } } func TestMoveWithMultipartCopy(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } rootDir, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(rootDir) d, err := s3DriverConstructor(rootDir, s3.StorageClassStandard) if err != nil { t.Fatalf("unexpected error creating driver: %v", err) } ctx := context.Background() sourcePath := "/source" destPath := "/dest" defer d.Delete(ctx, sourcePath) defer d.Delete(ctx, destPath) // An object larger than d's MultipartCopyThresholdSize will cause d.Move() to perform a multipart copy. multipartCopyThresholdSize := d.baseEmbed.Base.StorageDriver.(*driver).MultipartCopyThresholdSize contents := make([]byte, 2*multipartCopyThresholdSize) rand.Read(contents) err = d.PutContent(ctx, sourcePath, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } err = d.Move(ctx, sourcePath, destPath) if err != nil { t.Fatalf("unexpected error moving file: %v", err) } received, err := d.GetContent(ctx, destPath) if err != nil { t.Fatalf("unexpected error getting content: %v", err) } if !bytes.Equal(contents, received) { t.Fatal("content differs") } _, err = d.GetContent(ctx, sourcePath) switch err.(type) { case storagedriver.PathNotFoundError: default: t.Fatalf("unexpected error getting content: %v", err) } } docker-registry-2.6.2~ds1/registry/storage/driver/s3-aws/s3_v2_signer.go000066400000000000000000000137621313450123100262240ustar00rootroot00000000000000package s3 // Source: https://github.com/pivotal-golang/s3cli // Copyright (c) 2013 Damien Le Berrigaud and Nick Wade // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "net/http" "net/url" "sort" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/aws/aws-sdk-go/aws/corehandlers" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/s3" ) const ( signatureVersion = "2" signatureMethod = "HmacSHA1" timeFormat = "2006-01-02T15:04:05Z" ) type signer struct { // Values that must be populated from the request Request *http.Request Time time.Time Credentials *credentials.Credentials Query url.Values stringToSign string signature string } var s3ParamsToSign = map[string]bool{ "acl": true, "location": true, "logging": true, "notification": true, "partNumber": true, "policy": true, "requestPayment": true, "torrent": true, "uploadId": true, "uploads": true, "versionId": true, "versioning": true, "versions": true, "response-content-type": true, "response-content-language": true, "response-expires": true, "response-cache-control": true, "response-content-disposition": true, "response-content-encoding": true, "website": true, "delete": true, } // setv2Handlers will setup v2 signature signing on the S3 driver func setv2Handlers(svc *s3.S3) { svc.Handlers.Build.PushBack(func(r *request.Request) { parsedURL, err := url.Parse(r.HTTPRequest.URL.String()) if err != nil { log.Fatalf("Failed to parse URL: %v", err) } r.HTTPRequest.URL.Opaque = parsedURL.Path }) svc.Handlers.Sign.Clear() svc.Handlers.Sign.PushBack(Sign) svc.Handlers.Sign.PushBackNamed(corehandlers.BuildContentLengthHandler) } // Sign requests with signature version 2. // // Will sign the requests with the service config's Credentials object // Signing is skipped if the credentials is the credentials.AnonymousCredentials // object. func Sign(req *request.Request) { // If the request does not need to be signed ignore the signing of the // request if the AnonymousCredentials object is used. if req.Config.Credentials == credentials.AnonymousCredentials { return } v2 := signer{ Request: req.HTTPRequest, Time: req.Time, Credentials: req.Config.Credentials, } v2.Sign() } func (v2 *signer) Sign() error { credValue, err := v2.Credentials.Get() if err != nil { return err } accessKey := credValue.AccessKeyID var ( md5, ctype, date, xamz string xamzDate bool sarray []string smap map[string]string sharray []string ) headers := v2.Request.Header params := v2.Request.URL.Query() parsedURL, err := url.Parse(v2.Request.URL.String()) if err != nil { return err } host, canonicalPath := parsedURL.Host, parsedURL.Path v2.Request.Header["Host"] = []string{host} v2.Request.Header["date"] = []string{v2.Time.In(time.UTC).Format(time.RFC1123)} smap = make(map[string]string) for k, v := range headers { k = strings.ToLower(k) switch k { case "content-md5": md5 = v[0] case "content-type": ctype = v[0] case "date": if !xamzDate { date = v[0] } default: if strings.HasPrefix(k, "x-amz-") { vall := strings.Join(v, ",") smap[k] = k + ":" + vall if k == "x-amz-date" { xamzDate = true date = "" } sharray = append(sharray, k) } } } if len(sharray) > 0 { sort.StringSlice(sharray).Sort() for _, h := range sharray { sarray = append(sarray, smap[h]) } xamz = strings.Join(sarray, "\n") + "\n" } expires := false if v, ok := params["Expires"]; ok { expires = true date = v[0] params["AWSAccessKeyId"] = []string{accessKey} } sarray = sarray[0:0] for k, v := range params { if s3ParamsToSign[k] { for _, vi := range v { if vi == "" { sarray = append(sarray, k) } else { sarray = append(sarray, k+"="+vi) } } } } if len(sarray) > 0 { sort.StringSlice(sarray).Sort() canonicalPath = canonicalPath + "?" + strings.Join(sarray, "&") } v2.stringToSign = strings.Join([]string{ v2.Request.Method, md5, ctype, date, xamz + canonicalPath, }, "\n") hash := hmac.New(sha1.New, []byte(credValue.SecretAccessKey)) hash.Write([]byte(v2.stringToSign)) v2.signature = base64.StdEncoding.EncodeToString(hash.Sum(nil)) if expires { params["Signature"] = []string{string(v2.signature)} } else { headers["Authorization"] = []string{"AWS " + accessKey + ":" + string(v2.signature)} } log.WithFields(log.Fields{ "string-to-sign": v2.stringToSign, "signature": v2.signature, }).Debugln("request signature") return nil } docker-registry-2.6.2~ds1/registry/storage/driver/s3-goamz/000077500000000000000000000000001313450123100237045ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/s3-goamz/s3.go000066400000000000000000000505571313450123100245740ustar00rootroot00000000000000// Package s3 provides a storagedriver.StorageDriver implementation to // store blobs in Amazon S3 cloud storage. // // This package leverages the docker/goamz client library for interfacing with // S3. It is intended to be deprecated in favor of the s3-aws driver // implementation. // // Because S3 is a key, value store the Stat call does not support last modification // time for directories (directories are an abstraction for key, value stores) // // Keep in mind that S3 guarantees only read-after-write consistency for new // objects, but no read-after-update or list-after-write consistency. package s3 import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "reflect" "strconv" "strings" "time" "github.com/docker/goamz/aws" "github.com/docker/goamz/s3" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/client/transport" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" ) const driverName = "s3goamz" // minChunkSize defines the minimum multipart upload chunk size // S3 API requires multipart upload chunks to be at least 5MB const minChunkSize = 5 << 20 const defaultChunkSize = 2 * minChunkSize // listMax is the largest amount of objects you can request from S3 in a list call const listMax = 1000 //DriverParameters A struct that encapsulates all of the driver parameters after all values have been set type DriverParameters struct { AccessKey string SecretKey string Bucket string Region aws.Region Encrypt bool Secure bool V4Auth bool ChunkSize int64 RootDirectory string StorageClass s3.StorageClass UserAgent string } func init() { factory.Register(driverName, &s3DriverFactory{}) } // s3DriverFactory implements the factory.StorageDriverFactory interface type s3DriverFactory struct{} func (factory *s3DriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } type driver struct { S3 *s3.S3 Bucket *s3.Bucket ChunkSize int64 Encrypt bool RootDirectory string StorageClass s3.StorageClass } type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by Amazon S3 // Objects are stored at absolute keys in the provided bucket. type Driver struct { baseEmbed } // FromParameters constructs a new Driver with a given parameters map // Required parameters: // - accesskey // - secretkey // - region // - bucket // - encrypt func FromParameters(parameters map[string]interface{}) (*Driver, error) { // Providing no values for these is valid in case the user is authenticating // with an IAM on an ec2 instance (in which case the instance credentials will // be summoned when GetAuth is called) accessKey := parameters["accesskey"] if accessKey == nil { accessKey = "" } secretKey := parameters["secretkey"] if secretKey == nil { secretKey = "" } regionName := parameters["region"] if regionName == nil || fmt.Sprint(regionName) == "" { return nil, fmt.Errorf("No region parameter provided") } region := aws.GetRegion(fmt.Sprint(regionName)) if region.Name == "" { return nil, fmt.Errorf("Invalid region provided: %v", region) } bucket := parameters["bucket"] if bucket == nil || fmt.Sprint(bucket) == "" { return nil, fmt.Errorf("No bucket parameter provided") } encryptBool := false encrypt := parameters["encrypt"] switch encrypt := encrypt.(type) { case string: b, err := strconv.ParseBool(encrypt) if err != nil { return nil, fmt.Errorf("The encrypt parameter should be a boolean") } encryptBool = b case bool: encryptBool = encrypt case nil: // do nothing default: return nil, fmt.Errorf("The encrypt parameter should be a boolean") } secureBool := true secure := parameters["secure"] switch secure := secure.(type) { case string: b, err := strconv.ParseBool(secure) if err != nil { return nil, fmt.Errorf("The secure parameter should be a boolean") } secureBool = b case bool: secureBool = secure case nil: // do nothing default: return nil, fmt.Errorf("The secure parameter should be a boolean") } v4AuthBool := false v4Auth := parameters["v4auth"] switch v4Auth := v4Auth.(type) { case string: b, err := strconv.ParseBool(v4Auth) if err != nil { return nil, fmt.Errorf("The v4auth parameter should be a boolean") } v4AuthBool = b case bool: v4AuthBool = v4Auth case nil: // do nothing default: return nil, fmt.Errorf("The v4auth parameter should be a boolean") } chunkSize := int64(defaultChunkSize) chunkSizeParam := parameters["chunksize"] switch v := chunkSizeParam.(type) { case string: vv, err := strconv.ParseInt(v, 0, 64) if err != nil { return nil, fmt.Errorf("chunksize parameter must be an integer, %v invalid", chunkSizeParam) } chunkSize = vv case int64: chunkSize = v case int, uint, int32, uint32, uint64: chunkSize = reflect.ValueOf(v).Convert(reflect.TypeOf(chunkSize)).Int() case nil: // do nothing default: return nil, fmt.Errorf("invalid value for chunksize: %#v", chunkSizeParam) } if chunkSize < minChunkSize { return nil, fmt.Errorf("The chunksize %#v parameter should be a number that is larger than or equal to %d", chunkSize, minChunkSize) } rootDirectory := parameters["rootdirectory"] if rootDirectory == nil { rootDirectory = "" } storageClass := s3.StandardStorage storageClassParam := parameters["storageclass"] if storageClassParam != nil { storageClassString, ok := storageClassParam.(string) if !ok { return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []s3.StorageClass{s3.StandardStorage, s3.ReducedRedundancy}, storageClassParam) } // All valid storage class parameters are UPPERCASE, so be a bit more flexible here storageClassCasted := s3.StorageClass(strings.ToUpper(storageClassString)) if storageClassCasted != s3.StandardStorage && storageClassCasted != s3.ReducedRedundancy { return nil, fmt.Errorf("The storageclass parameter must be one of %v, %v invalid", []s3.StorageClass{s3.StandardStorage, s3.ReducedRedundancy}, storageClassParam) } storageClass = storageClassCasted } userAgent := parameters["useragent"] if userAgent == nil { userAgent = "" } params := DriverParameters{ fmt.Sprint(accessKey), fmt.Sprint(secretKey), fmt.Sprint(bucket), region, encryptBool, secureBool, v4AuthBool, chunkSize, fmt.Sprint(rootDirectory), storageClass, fmt.Sprint(userAgent), } return New(params) } // New constructs a new Driver with the given AWS credentials, region, encryption flag, and // bucketName func New(params DriverParameters) (*Driver, error) { auth, err := aws.GetAuth(params.AccessKey, params.SecretKey, "", time.Time{}) if err != nil { return nil, fmt.Errorf("unable to resolve aws credentials, please ensure that 'accesskey' and 'secretkey' are properly set or the credentials are available in $HOME/.aws/credentials: %v", err) } if !params.Secure { params.Region.S3Endpoint = strings.Replace(params.Region.S3Endpoint, "https", "http", 1) } s3obj := s3.New(auth, params.Region) if params.UserAgent != "" { s3obj.Client = &http.Client{ Transport: transport.NewTransport(http.DefaultTransport, transport.NewHeaderRequestModifier(http.Header{ http.CanonicalHeaderKey("User-Agent"): []string{params.UserAgent}, }), ), } } if params.V4Auth { s3obj.Signature = aws.V4Signature } else { if params.Region.Name == "eu-central-1" { return nil, fmt.Errorf("The eu-central-1 region only works with v4 authentication") } } bucket := s3obj.Bucket(params.Bucket) // TODO Currently multipart uploads have no timestamps, so this would be unwise // if you initiated a new s3driver while another one is running on the same bucket. // multis, _, err := bucket.ListMulti("", "") // if err != nil { // return nil, err // } // for _, multi := range multis { // err := multi.Abort() // //TODO appropriate to do this error checking? // if err != nil { // return nil, err // } // } d := &driver{ S3: s3obj, Bucket: bucket, ChunkSize: params.ChunkSize, Encrypt: params.Encrypt, RootDirectory: params.RootDirectory, StorageClass: params.StorageClass, } return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: d, }, }, }, nil } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { content, err := d.Bucket.Get(d.s3Path(path)) if err != nil { return nil, parseError(path, err) } return content, nil } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { return parseError(path, d.Bucket.Put(d.s3Path(path), contents, d.getContentType(), getPermissions(), d.getOptions())) } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { headers := make(http.Header) headers.Add("Range", "bytes="+strconv.FormatInt(offset, 10)+"-") resp, err := d.Bucket.GetResponseWithHeaders(d.s3Path(path), headers) if err != nil { if s3Err, ok := err.(*s3.Error); ok && s3Err.Code == "InvalidRange" { return ioutil.NopCloser(bytes.NewReader(nil)), nil } return nil, parseError(path, err) } return resp.Body, nil } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { key := d.s3Path(path) if !append { // TODO (brianbland): cancel other uploads at this path multi, err := d.Bucket.InitMulti(key, d.getContentType(), getPermissions(), d.getOptions()) if err != nil { return nil, err } return d.newWriter(key, multi, nil), nil } multis, _, err := d.Bucket.ListMulti(key, "") if err != nil { return nil, parseError(path, err) } for _, multi := range multis { if key != multi.Key { continue } parts, err := multi.ListParts() if err != nil { return nil, parseError(path, err) } var multiSize int64 for _, part := range parts { multiSize += part.Size } return d.newWriter(key, multi, parts), nil } return nil, storagedriver.PathNotFoundError{Path: path} } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { listResponse, err := d.Bucket.List(d.s3Path(path), "", "", 1) if err != nil { return nil, err } fi := storagedriver.FileInfoFields{ Path: path, } if len(listResponse.Contents) == 1 { if listResponse.Contents[0].Key != d.s3Path(path) { fi.IsDir = true } else { fi.IsDir = false fi.Size = listResponse.Contents[0].Size timestamp, err := time.Parse(time.RFC3339Nano, listResponse.Contents[0].LastModified) if err != nil { return nil, err } fi.ModTime = timestamp } } else if len(listResponse.CommonPrefixes) == 1 { fi.IsDir = true } else { return nil, storagedriver.PathNotFoundError{Path: path} } return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } // List returns a list of the objects that are direct descendants of the given path. func (d *driver) List(ctx context.Context, opath string) ([]string, error) { path := opath if path != "/" && path[len(path)-1] != '/' { path = path + "/" } // This is to cover for the cases when the rootDirectory of the driver is either "" or "/". // In those cases, there is no root prefix to replace and we must actually add a "/" to all // results in order to keep them as valid paths as recognized by storagedriver.PathRegexp prefix := "" if d.s3Path("") == "" { prefix = "/" } listResponse, err := d.Bucket.List(d.s3Path(path), "/", "", listMax) if err != nil { return nil, parseError(opath, err) } files := []string{} directories := []string{} for { for _, key := range listResponse.Contents { files = append(files, strings.Replace(key.Key, d.s3Path(""), prefix, 1)) } for _, commonPrefix := range listResponse.CommonPrefixes { directories = append(directories, strings.Replace(commonPrefix[0:len(commonPrefix)-1], d.s3Path(""), prefix, 1)) } if listResponse.IsTruncated { listResponse, err = d.Bucket.List(d.s3Path(path), "/", listResponse.NextMarker, listMax) if err != nil { return nil, err } } else { break } } if opath != "/" { if len(files) == 0 && len(directories) == 0 { // Treat empty response as missing directory, since we don't actually // have directories in s3. return nil, storagedriver.PathNotFoundError{Path: opath} } } return append(files, directories...), nil } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { /* This is terrible, but aws doesn't have an actual move. */ _, err := d.Bucket.PutCopy(d.s3Path(destPath), getPermissions(), s3.CopyOptions{Options: d.getOptions(), ContentType: d.getContentType()}, d.Bucket.Name+"/"+d.s3Path(sourcePath)) if err != nil { return parseError(sourcePath, err) } return d.Delete(ctx, sourcePath) } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, path string) error { s3Path := d.s3Path(path) listResponse, err := d.Bucket.List(s3Path, "", "", listMax) if err != nil || len(listResponse.Contents) == 0 { return storagedriver.PathNotFoundError{Path: path} } s3Objects := make([]s3.Object, listMax) for len(listResponse.Contents) > 0 { numS3Objects := len(listResponse.Contents) for index, key := range listResponse.Contents { // Stop if we encounter a key that is not a subpath (so that deleting "/a" does not delete "/ab"). if len(key.Key) > len(s3Path) && (key.Key)[len(s3Path)] != '/' { numS3Objects = index break } s3Objects[index].Key = key.Key } err := d.Bucket.DelMulti(s3.Delete{Quiet: false, Objects: s3Objects[0:numS3Objects]}) if err != nil { return nil } if numS3Objects < len(listResponse.Contents) { return nil } listResponse, err = d.Bucket.List(d.s3Path(path), "", "", listMax) if err != nil { return err } } return nil } // URLFor returns a URL which may be used to retrieve the content stored at the given path. // May return an UnsupportedMethodErr in certain StorageDriver implementations. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { methodString := "GET" method, ok := options["method"] if ok { methodString, ok = method.(string) if !ok || (methodString != "GET" && methodString != "HEAD") { return "", storagedriver.ErrUnsupportedMethod{} } } expiresTime := time.Now().Add(20 * time.Minute) expires, ok := options["expiry"] if ok { et, ok := expires.(time.Time) if ok { expiresTime = et } } return d.Bucket.SignedURLWithMethod(methodString, d.s3Path(path), expiresTime, nil, nil), nil } func (d *driver) s3Path(path string) string { return strings.TrimLeft(strings.TrimRight(d.RootDirectory, "/")+path, "/") } // S3BucketKey returns the s3 bucket key for the given storage driver path. func (d *Driver) S3BucketKey(path string) string { return d.StorageDriver.(*driver).s3Path(path) } func parseError(path string, err error) error { if s3Err, ok := err.(*s3.Error); ok && s3Err.Code == "NoSuchKey" { return storagedriver.PathNotFoundError{Path: path} } return err } func hasCode(err error, code string) bool { s3err, ok := err.(*aws.Error) return ok && s3err.Code == code } func (d *driver) getOptions() s3.Options { return s3.Options{ SSE: d.Encrypt, StorageClass: d.StorageClass, } } func getPermissions() s3.ACL { return s3.Private } func (d *driver) getContentType() string { return "application/octet-stream" } // writer attempts to upload parts to S3 in a buffered fashion where the last // part is at least as large as the chunksize, so the multipart upload could be // cleanly resumed in the future. This is violated if Close is called after less // than a full chunk is written. type writer struct { driver *driver key string multi *s3.Multi parts []s3.Part size int64 readyPart []byte pendingPart []byte closed bool committed bool cancelled bool } func (d *driver) newWriter(key string, multi *s3.Multi, parts []s3.Part) storagedriver.FileWriter { var size int64 for _, part := range parts { size += part.Size } return &writer{ driver: d, key: key, multi: multi, parts: parts, size: size, } } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } // If the last written part is smaller than minChunkSize, we need to make a // new multipart upload :sadface: if len(w.parts) > 0 && int(w.parts[len(w.parts)-1].Size) < minChunkSize { err := w.multi.Complete(w.parts) if err != nil { w.multi.Abort() return 0, err } multi, err := w.driver.Bucket.InitMulti(w.key, w.driver.getContentType(), getPermissions(), w.driver.getOptions()) if err != nil { return 0, err } w.multi = multi // If the entire written file is smaller than minChunkSize, we need to make // a new part from scratch :double sad face: if w.size < minChunkSize { contents, err := w.driver.Bucket.Get(w.key) if err != nil { return 0, err } w.parts = nil w.readyPart = contents } else { // Otherwise we can use the old file as the new first part _, part, err := multi.PutPartCopy(1, s3.CopyOptions{}, w.driver.Bucket.Name+"/"+w.key) if err != nil { return 0, err } w.parts = []s3.Part{part} } } var n int for len(p) > 0 { // If no parts are ready to write, fill up the first part if neededBytes := int(w.driver.ChunkSize) - len(w.readyPart); neededBytes > 0 { if len(p) >= neededBytes { w.readyPart = append(w.readyPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] } else { w.readyPart = append(w.readyPart, p...) n += len(p) p = nil } } if neededBytes := int(w.driver.ChunkSize) - len(w.pendingPart); neededBytes > 0 { if len(p) >= neededBytes { w.pendingPart = append(w.pendingPart, p[:neededBytes]...) n += neededBytes p = p[neededBytes:] err := w.flushPart() if err != nil { w.size += int64(n) return n, err } } else { w.pendingPart = append(w.pendingPart, p...) n += len(p) p = nil } } } w.size += int64(n) return n, nil } func (w *writer) Size() int64 { return w.size } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } w.closed = true return w.flushPart() } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true err := w.multi.Abort() return err } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } err := w.flushPart() if err != nil { return err } w.committed = true err = w.multi.Complete(w.parts) if err != nil { w.multi.Abort() return err } return nil } // flushPart flushes buffers to write a part to S3. // Only called by Write (with both buffers full) and Close/Commit (always) func (w *writer) flushPart() error { if len(w.readyPart) == 0 && len(w.pendingPart) == 0 { // nothing to write return nil } if len(w.pendingPart) < int(w.driver.ChunkSize) { // closing with a small pending part // combine ready and pending to avoid writing a small part w.readyPart = append(w.readyPart, w.pendingPart...) w.pendingPart = nil } part, err := w.multi.PutPart(len(w.parts)+1, bytes.NewReader(w.readyPart)) if err != nil { return err } w.parts = append(w.parts, part) w.readyPart = w.pendingPart w.pendingPart = nil return nil } docker-registry-2.6.2~ds1/registry/storage/driver/s3-goamz/s3_test.go000066400000000000000000000130561313450123100256240ustar00rootroot00000000000000package s3 import ( "io/ioutil" "os" "strconv" "testing" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" "github.com/docker/goamz/aws" "github.com/docker/goamz/s3" "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } var s3DriverConstructor func(rootDirectory string, storageClass s3.StorageClass) (*Driver, error) var skipS3 func() string func init() { accessKey := os.Getenv("AWS_ACCESS_KEY") secretKey := os.Getenv("AWS_SECRET_KEY") bucket := os.Getenv("S3_BUCKET") encrypt := os.Getenv("S3_ENCRYPT") secure := os.Getenv("S3_SECURE") v4auth := os.Getenv("S3_USE_V4_AUTH") region := os.Getenv("AWS_REGION") root, err := ioutil.TempDir("", "driver-") if err != nil { panic(err) } defer os.Remove(root) s3DriverConstructor = func(rootDirectory string, storageClass s3.StorageClass) (*Driver, error) { encryptBool := false if encrypt != "" { encryptBool, err = strconv.ParseBool(encrypt) if err != nil { return nil, err } } secureBool := true if secure != "" { secureBool, err = strconv.ParseBool(secure) if err != nil { return nil, err } } v4AuthBool := false if v4auth != "" { v4AuthBool, err = strconv.ParseBool(v4auth) if err != nil { return nil, err } } parameters := DriverParameters{ accessKey, secretKey, bucket, aws.GetRegion(region), encryptBool, secureBool, v4AuthBool, minChunkSize, rootDirectory, storageClass, driverName + "-test", } return New(parameters) } // Skip S3 storage driver tests if environment variable parameters are not provided skipS3 = func() string { if accessKey == "" || secretKey == "" || region == "" || bucket == "" || encrypt == "" { return "Must set AWS_ACCESS_KEY, AWS_SECRET_KEY, AWS_REGION, S3_BUCKET, and S3_ENCRYPT to run S3 tests" } return "" } testsuites.RegisterSuite(func() (storagedriver.StorageDriver, error) { return s3DriverConstructor(root, s3.StandardStorage) }, skipS3) } func TestEmptyRootList(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) rootedDriver, err := s3DriverConstructor(validRoot, s3.StandardStorage) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } emptyRootDriver, err := s3DriverConstructor("", s3.StandardStorage) if err != nil { t.Fatalf("unexpected error creating empty root driver: %v", err) } slashRootDriver, err := s3DriverConstructor("/", s3.StandardStorage) if err != nil { t.Fatalf("unexpected error creating slash root driver: %v", err) } filename := "/test" contents := []byte("contents") ctx := context.Background() err = rootedDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer rootedDriver.Delete(ctx, filename) keys, err := emptyRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } keys, err = slashRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } } func TestStorageClass(t *testing.T) { if skipS3() != "" { t.Skip(skipS3()) } rootDir, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(rootDir) standardDriver, err := s3DriverConstructor(rootDir, s3.StandardStorage) if err != nil { t.Fatalf("unexpected error creating driver with standard storage: %v", err) } rrDriver, err := s3DriverConstructor(rootDir, s3.ReducedRedundancy) if err != nil { t.Fatalf("unexpected error creating driver with reduced redundancy storage: %v", err) } standardFilename := "/test-standard" rrFilename := "/test-rr" contents := []byte("contents") ctx := context.Background() err = standardDriver.PutContent(ctx, standardFilename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer standardDriver.Delete(ctx, standardFilename) err = rrDriver.PutContent(ctx, rrFilename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } defer rrDriver.Delete(ctx, rrFilename) standardDriverUnwrapped := standardDriver.Base.StorageDriver.(*driver) resp, err := standardDriverUnwrapped.Bucket.GetResponse(standardDriverUnwrapped.s3Path(standardFilename)) if err != nil { t.Fatalf("unexpected error retrieving standard storage file: %v", err) } defer resp.Body.Close() // Amazon only populates this header value for non-standard storage classes if storageClass := resp.Header.Get("x-amz-storage-class"); storageClass != "" { t.Fatalf("unexpected storage class for standard file: %v", storageClass) } rrDriverUnwrapped := rrDriver.Base.StorageDriver.(*driver) resp, err = rrDriverUnwrapped.Bucket.GetResponse(rrDriverUnwrapped.s3Path(rrFilename)) if err != nil { t.Fatalf("unexpected error retrieving reduced-redundancy storage file: %v", err) } defer resp.Body.Close() if storageClass := resp.Header.Get("x-amz-storage-class"); storageClass != string(s3.ReducedRedundancy) { t.Fatalf("unexpected storage class for reduced-redundancy file: %v", storageClass) } } docker-registry-2.6.2~ds1/registry/storage/driver/storagedriver.go000066400000000000000000000135231313450123100254570ustar00rootroot00000000000000package driver import ( "fmt" "io" "regexp" "strconv" "strings" "github.com/docker/distribution/context" ) // Version is a string representing the storage driver version, of the form // Major.Minor. // The registry must accept storage drivers with equal major version and greater // minor version, but may not be compatible with older storage driver versions. type Version string // Major returns the major (primary) component of a version. func (version Version) Major() uint { majorPart := strings.Split(string(version), ".")[0] major, _ := strconv.ParseUint(majorPart, 10, 0) return uint(major) } // Minor returns the minor (secondary) component of a version. func (version Version) Minor() uint { minorPart := strings.Split(string(version), ".")[1] minor, _ := strconv.ParseUint(minorPart, 10, 0) return uint(minor) } // CurrentVersion is the current storage driver Version. const CurrentVersion Version = "0.1" // StorageDriver defines methods that a Storage Driver must implement for a // filesystem-like key/value object storage. Storage Drivers are automatically // registered via an internal registration mechanism, and generally created // via the StorageDriverFactory interface (https://godoc.org/github.com/docker/distribution/registry/storage/driver/factory). // Please see the aforementioned factory package for example code showing how to get an instance // of a StorageDriver type StorageDriver interface { // Name returns the human-readable "name" of the driver, useful in error // messages and logging. By convention, this will just be the registration // name, but drivers may provide other information here. Name() string // GetContent retrieves the content stored at "path" as a []byte. // This should primarily be used for small objects. GetContent(ctx context.Context, path string) ([]byte, error) // PutContent stores the []byte content at a location designated by "path". // This should primarily be used for small objects. PutContent(ctx context.Context, path string, content []byte) error // Reader retrieves an io.ReadCloser for the content stored at "path" // with a given byte offset. // May be used to resume reading a stream by providing a nonzero offset. Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. Writer(ctx context.Context, path string, append bool) (FileWriter, error) // Stat retrieves the FileInfo for the given path, including the current // size in bytes and the creation time. Stat(ctx context.Context, path string) (FileInfo, error) // List returns a list of the objects that are direct descendants of the //given path. List(ctx context.Context, path string) ([]string, error) // Move moves an object stored at sourcePath to destPath, removing the // original object. // Note: This may be no more efficient than a copy followed by a delete for // many implementations. Move(ctx context.Context, sourcePath string, destPath string) error // Delete recursively deletes all objects stored at "path" and its subpaths. Delete(ctx context.Context, path string) error // URLFor returns a URL which may be used to retrieve the content stored at // the given path, possibly using the given options. // May return an ErrUnsupportedMethod in certain StorageDriver // implementations. URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) } // FileWriter provides an abstraction for an opened writable file-like object in // the storage backend. The FileWriter must flush all content written to it on // the call to Close, but is only required to make its content readable on a // call to Commit. type FileWriter interface { io.WriteCloser // Size returns the number of bytes written to this FileWriter. Size() int64 // Cancel removes any written content from this FileWriter. Cancel() error // Commit flushes all content written to this FileWriter and makes it // available for future calls to StorageDriver.GetContent and // StorageDriver.Reader. Commit() error } // PathRegexp is the regular expression which each file path must match. A // file path is absolute, beginning with a slash and containing a positive // number of path components separated by slashes, where each component is // restricted to alphanumeric characters or a period, underscore, or // hyphen. var PathRegexp = regexp.MustCompile(`^(/[A-Za-z0-9._-]+)+$`) // ErrUnsupportedMethod may be returned in the case where a StorageDriver implementation does not support an optional method. type ErrUnsupportedMethod struct { DriverName string } func (err ErrUnsupportedMethod) Error() string { return fmt.Sprintf("%s: unsupported method", err.DriverName) } // PathNotFoundError is returned when operating on a nonexistent path. type PathNotFoundError struct { Path string DriverName string } func (err PathNotFoundError) Error() string { return fmt.Sprintf("%s: Path not found: %s", err.DriverName, err.Path) } // InvalidPathError is returned when the provided path is malformed. type InvalidPathError struct { Path string DriverName string } func (err InvalidPathError) Error() string { return fmt.Sprintf("%s: invalid path: %s", err.DriverName, err.Path) } // InvalidOffsetError is returned when attempting to read or write from an // invalid offset. type InvalidOffsetError struct { Path string Offset int64 DriverName string } func (err InvalidOffsetError) Error() string { return fmt.Sprintf("%s: invalid offset: %d for path: %s", err.DriverName, err.Offset, err.Path) } // Error is a catch-all error type which captures an error string and // the driver type on which it occurred. type Error struct { DriverName string Enclosed error } func (err Error) Error() string { return fmt.Sprintf("%s: %s", err.DriverName, err.Enclosed) } docker-registry-2.6.2~ds1/registry/storage/driver/swift/000077500000000000000000000000001313450123100234005ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/swift/swift.go000066400000000000000000000624111313450123100250670ustar00rootroot00000000000000// Package swift provides a storagedriver.StorageDriver implementation to // store blobs in Openstack Swift object storage. // // This package leverages the ncw/swift client library for interfacing with // Swift. // // It supports both TempAuth authentication and Keystone authentication // (up to version 3). // // As Swift has a limit on the size of a single uploaded object (by default // this is 5GB), the driver makes use of the Swift Large Object Support // (http://docs.openstack.org/developer/swift/overview_large_objects.html). // Only one container is used for both manifests and data objects. Manifests // are stored in the 'files' pseudo directory, data objects are stored under // 'segments'. package swift import ( "bufio" "bytes" "crypto/rand" "crypto/sha1" "crypto/tls" "encoding/hex" "fmt" "io" "io/ioutil" "net/http" "net/url" "strconv" "strings" "time" "github.com/mitchellh/mapstructure" "github.com/ncw/swift" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/base" "github.com/docker/distribution/registry/storage/driver/factory" "github.com/docker/distribution/version" ) const driverName = "swift" // defaultChunkSize defines the default size of a segment const defaultChunkSize = 20 * 1024 * 1024 // minChunkSize defines the minimum size of a segment const minChunkSize = 1 << 20 // contentType defines the Content-Type header associated with stored segments const contentType = "application/octet-stream" // readAfterWriteTimeout defines the time we wait before an object appears after having been uploaded var readAfterWriteTimeout = 15 * time.Second // readAfterWriteWait defines the time to sleep between two retries var readAfterWriteWait = 200 * time.Millisecond // Parameters A struct that encapsulates all of the driver parameters after all values have been set type Parameters struct { Username string Password string AuthURL string Tenant string TenantID string Domain string DomainID string TenantDomain string TenantDomainID string TrustID string Region string AuthVersion int Container string Prefix string EndpointType string InsecureSkipVerify bool ChunkSize int SecretKey string AccessKey string TempURLContainerKey bool TempURLMethods []string } // swiftInfo maps the JSON structure returned by Swift /info endpoint type swiftInfo struct { Swift struct { Version string `mapstructure:"version"` } Tempurl struct { Methods []string `mapstructure:"methods"` } BulkDelete struct { MaxDeletesPerRequest int `mapstructure:"max_deletes_per_request"` } `mapstructure:"bulk_delete"` } func init() { factory.Register(driverName, &swiftDriverFactory{}) } // swiftDriverFactory implements the factory.StorageDriverFactory interface type swiftDriverFactory struct{} func (factory *swiftDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return FromParameters(parameters) } type driver struct { Conn *swift.Connection Container string Prefix string BulkDeleteSupport bool BulkDeleteMaxDeletes int ChunkSize int SecretKey string AccessKey string TempURLContainerKey bool TempURLMethods []string } type baseEmbed struct { base.Base } // Driver is a storagedriver.StorageDriver implementation backed by Openstack Swift // Objects are stored at absolute keys in the provided container. type Driver struct { baseEmbed } // FromParameters constructs a new Driver with a given parameters map // Required parameters: // - username // - password // - authurl // - container func FromParameters(parameters map[string]interface{}) (*Driver, error) { params := Parameters{ ChunkSize: defaultChunkSize, InsecureSkipVerify: false, } if err := mapstructure.Decode(parameters, ¶ms); err != nil { return nil, err } if params.Username == "" { return nil, fmt.Errorf("No username parameter provided") } if params.Password == "" { return nil, fmt.Errorf("No password parameter provided") } if params.AuthURL == "" { return nil, fmt.Errorf("No authurl parameter provided") } if params.Container == "" { return nil, fmt.Errorf("No container parameter provided") } if params.ChunkSize < minChunkSize { return nil, fmt.Errorf("The chunksize %#v parameter should be a number that is larger than or equal to %d", params.ChunkSize, minChunkSize) } return New(params) } // New constructs a new Driver with the given Openstack Swift credentials and container name func New(params Parameters) (*Driver, error) { transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, MaxIdleConnsPerHost: 2048, TLSClientConfig: &tls.Config{InsecureSkipVerify: params.InsecureSkipVerify}, } ct := &swift.Connection{ UserName: params.Username, ApiKey: params.Password, AuthUrl: params.AuthURL, Region: params.Region, AuthVersion: params.AuthVersion, UserAgent: "distribution/" + version.Version, Tenant: params.Tenant, TenantId: params.TenantID, Domain: params.Domain, DomainId: params.DomainID, TenantDomain: params.TenantDomain, TenantDomainId: params.TenantDomainID, TrustId: params.TrustID, EndpointType: swift.EndpointType(params.EndpointType), Transport: transport, ConnectTimeout: 60 * time.Second, Timeout: 15 * 60 * time.Second, } err := ct.Authenticate() if err != nil { return nil, fmt.Errorf("Swift authentication failed: %s", err) } if _, _, err := ct.Container(params.Container); err == swift.ContainerNotFound { if err := ct.ContainerCreate(params.Container, nil); err != nil { return nil, fmt.Errorf("Failed to create container %s (%s)", params.Container, err) } } else if err != nil { return nil, fmt.Errorf("Failed to retrieve info about container %s (%s)", params.Container, err) } d := &driver{ Conn: ct, Container: params.Container, Prefix: params.Prefix, ChunkSize: params.ChunkSize, TempURLMethods: make([]string, 0), AccessKey: params.AccessKey, } info := swiftInfo{} if config, err := d.Conn.QueryInfo(); err == nil { _, d.BulkDeleteSupport = config["bulk_delete"] if err := mapstructure.Decode(config, &info); err == nil { d.TempURLContainerKey = info.Swift.Version >= "2.3.0" d.TempURLMethods = info.Tempurl.Methods if d.BulkDeleteSupport { d.BulkDeleteMaxDeletes = info.BulkDelete.MaxDeletesPerRequest } } } else { d.TempURLContainerKey = params.TempURLContainerKey d.TempURLMethods = params.TempURLMethods } if len(d.TempURLMethods) > 0 { secretKey := params.SecretKey if secretKey == "" { secretKey, _ = generateSecret() } // Since Swift 2.2.2, we can now set secret keys on containers // in addition to the account secret keys. Use them in preference. if d.TempURLContainerKey { _, containerHeaders, err := d.Conn.Container(d.Container) if err != nil { return nil, fmt.Errorf("Failed to fetch container info %s (%s)", d.Container, err) } d.SecretKey = containerHeaders["X-Container-Meta-Temp-Url-Key"] if d.SecretKey == "" || (params.SecretKey != "" && d.SecretKey != params.SecretKey) { m := swift.Metadata{} m["temp-url-key"] = secretKey if d.Conn.ContainerUpdate(d.Container, m.ContainerHeaders()); err == nil { d.SecretKey = secretKey } } } else { // Use the account secret key _, accountHeaders, err := d.Conn.Account() if err != nil { return nil, fmt.Errorf("Failed to fetch account info (%s)", err) } d.SecretKey = accountHeaders["X-Account-Meta-Temp-Url-Key"] if d.SecretKey == "" || (params.SecretKey != "" && d.SecretKey != params.SecretKey) { m := swift.Metadata{} m["temp-url-key"] = secretKey if err := d.Conn.AccountUpdate(m.AccountHeaders()); err == nil { d.SecretKey = secretKey } } } } return &Driver{ baseEmbed: baseEmbed{ Base: base.Base{ StorageDriver: d, }, }, }, nil } // Implement the storagedriver.StorageDriver interface func (d *driver) Name() string { return driverName } // GetContent retrieves the content stored at "path" as a []byte. func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { content, err := d.Conn.ObjectGetBytes(d.Container, d.swiftPath(path)) if err == swift.ObjectNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } return content, err } // PutContent stores the []byte content at a location designated by "path". func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { err := d.Conn.ObjectPutBytes(d.Container, d.swiftPath(path), contents, contentType) if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } // Reader retrieves an io.ReadCloser for the content stored at "path" with a // given byte offset. func (d *driver) Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { headers := make(swift.Headers) headers["Range"] = "bytes=" + strconv.FormatInt(offset, 10) + "-" waitingTime := readAfterWriteWait endTime := time.Now().Add(readAfterWriteTimeout) for { file, headers, err := d.Conn.ObjectOpen(d.Container, d.swiftPath(path), false, headers) if err != nil { if err == swift.ObjectNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } if swiftErr, ok := err.(*swift.Error); ok && swiftErr.StatusCode == http.StatusRequestedRangeNotSatisfiable { return ioutil.NopCloser(bytes.NewReader(nil)), nil } return file, err } //if this is a DLO and it is clear that segments are still missing, //wait until they show up _, isDLO := headers["X-Object-Manifest"] size, err := file.Length() if err != nil { return file, err } if isDLO && size == 0 { if time.Now().Add(waitingTime).After(endTime) { return nil, fmt.Errorf("Timeout expired while waiting for segments of %s to show up", path) } time.Sleep(waitingTime) waitingTime *= 2 continue } //if not, then this reader will be fine return file, nil } } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (d *driver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { var ( segments []swift.Object segmentsPath string err error ) if !append { segmentsPath, err = d.swiftSegmentPath(path) if err != nil { return nil, err } } else { info, headers, err := d.Conn.Object(d.Container, d.swiftPath(path)) if err == swift.ObjectNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } else if err != nil { return nil, err } manifest, ok := headers["X-Object-Manifest"] if !ok { segmentsPath, err = d.swiftSegmentPath(path) if err != nil { return nil, err } if err := d.Conn.ObjectMove(d.Container, d.swiftPath(path), d.Container, getSegmentPath(segmentsPath, len(segments))); err != nil { return nil, err } segments = []swift.Object{info} } else { _, segmentsPath = parseManifest(manifest) if segments, err = d.getAllSegments(segmentsPath); err != nil { return nil, err } } } return d.newWriter(path, segmentsPath, segments), nil } // Stat retrieves the FileInfo for the given path, including the current size // in bytes and the creation time. func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { swiftPath := d.swiftPath(path) opts := &swift.ObjectsOpts{ Prefix: swiftPath, Delimiter: '/', } objects, err := d.Conn.ObjectsAll(d.Container, opts) if err != nil { if err == swift.ContainerNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } return nil, err } fi := storagedriver.FileInfoFields{ Path: strings.TrimPrefix(strings.TrimSuffix(swiftPath, "/"), d.swiftPath("/")), } for _, obj := range objects { if obj.PseudoDirectory && obj.Name == swiftPath+"/" { fi.IsDir = true return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } else if obj.Name == swiftPath { // The file exists. But on Swift 1.12, the 'bytes' field is always 0 so // we need to do a separate HEAD request. break } } //Don't trust an empty `objects` slice. A container listing can be //outdated. For files, we can make a HEAD request on the object which //reports existence (at least) much more reliably. waitingTime := readAfterWriteWait endTime := time.Now().Add(readAfterWriteTimeout) for { info, headers, err := d.Conn.Object(d.Container, swiftPath) if err != nil { if err == swift.ObjectNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } return nil, err } //if this is a DLO and it is clear that segments are still missing, //wait until they show up _, isDLO := headers["X-Object-Manifest"] if isDLO && info.Bytes == 0 { if time.Now().Add(waitingTime).After(endTime) { return nil, fmt.Errorf("Timeout expired while waiting for segments of %s to show up", path) } time.Sleep(waitingTime) waitingTime *= 2 continue } //otherwise, accept the result fi.IsDir = false fi.Size = info.Bytes fi.ModTime = info.LastModified return storagedriver.FileInfoInternal{FileInfoFields: fi}, nil } } // List returns a list of the objects that are direct descendants of the given path. func (d *driver) List(ctx context.Context, path string) ([]string, error) { var files []string prefix := d.swiftPath(path) if prefix != "" { prefix += "/" } opts := &swift.ObjectsOpts{ Prefix: prefix, Delimiter: '/', } objects, err := d.Conn.ObjectsAll(d.Container, opts) for _, obj := range objects { files = append(files, strings.TrimPrefix(strings.TrimSuffix(obj.Name, "/"), d.swiftPath("/"))) } if err == swift.ContainerNotFound || (len(objects) == 0 && path != "/") { return files, storagedriver.PathNotFoundError{Path: path} } return files, err } // Move moves an object stored at sourcePath to destPath, removing the original // object. func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { _, headers, err := d.Conn.Object(d.Container, d.swiftPath(sourcePath)) if err == nil { if manifest, ok := headers["X-Object-Manifest"]; ok { if err = d.createManifest(destPath, manifest); err != nil { return err } err = d.Conn.ObjectDelete(d.Container, d.swiftPath(sourcePath)) } else { err = d.Conn.ObjectMove(d.Container, d.swiftPath(sourcePath), d.Container, d.swiftPath(destPath)) } } if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: sourcePath} } return err } // Delete recursively deletes all objects stored at "path" and its subpaths. func (d *driver) Delete(ctx context.Context, path string) error { opts := swift.ObjectsOpts{ Prefix: d.swiftPath(path) + "/", } objects, err := d.Conn.ObjectsAll(d.Container, &opts) if err != nil { if err == swift.ContainerNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } for _, obj := range objects { if obj.PseudoDirectory { continue } if _, headers, err := d.Conn.Object(d.Container, obj.Name); err == nil { manifest, ok := headers["X-Object-Manifest"] if ok { _, prefix := parseManifest(manifest) segments, err := d.getAllSegments(prefix) if err != nil { return err } objects = append(objects, segments...) } } else { if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: obj.Name} } return err } } if d.BulkDeleteSupport && len(objects) > 0 && d.BulkDeleteMaxDeletes > 0 { filenames := make([]string, len(objects)) for i, obj := range objects { filenames[i] = obj.Name } chunks, err := chunkFilenames(filenames, d.BulkDeleteMaxDeletes) if err != nil { return err } for _, chunk := range chunks { _, err := d.Conn.BulkDelete(d.Container, chunk) // Don't fail on ObjectNotFound because eventual consistency // makes this situation normal. if err != nil && err != swift.Forbidden && err != swift.ObjectNotFound { if err == swift.ContainerNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } } } else { for _, obj := range objects { if err := d.Conn.ObjectDelete(d.Container, obj.Name); err != nil { if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: obj.Name} } return err } } } _, _, err = d.Conn.Object(d.Container, d.swiftPath(path)) if err == nil { if err := d.Conn.ObjectDelete(d.Container, d.swiftPath(path)); err != nil { if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } } else if err == swift.ObjectNotFound { if len(objects) == 0 { return storagedriver.PathNotFoundError{Path: path} } } else { return err } return nil } // URLFor returns a URL which may be used to retrieve the content stored at the given path. func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { if d.SecretKey == "" { return "", storagedriver.ErrUnsupportedMethod{} } methodString := "GET" method, ok := options["method"] if ok { if methodString, ok = method.(string); !ok { return "", storagedriver.ErrUnsupportedMethod{} } } if methodString == "HEAD" { // A "HEAD" request on a temporary URL is allowed if the // signature was generated with "GET", "POST" or "PUT" methodString = "GET" } supported := false for _, method := range d.TempURLMethods { if method == methodString { supported = true break } } if !supported { return "", storagedriver.ErrUnsupportedMethod{} } expiresTime := time.Now().Add(20 * time.Minute) expires, ok := options["expiry"] if ok { et, ok := expires.(time.Time) if ok { expiresTime = et } } tempURL := d.Conn.ObjectTempUrl(d.Container, d.swiftPath(path), d.SecretKey, methodString, expiresTime) if d.AccessKey != "" { // On HP Cloud, the signature must be in the form of tenant_id:access_key:signature url, _ := url.Parse(tempURL) query := url.Query() query.Set("temp_url_sig", fmt.Sprintf("%s:%s:%s", d.Conn.TenantId, d.AccessKey, query.Get("temp_url_sig"))) url.RawQuery = query.Encode() tempURL = url.String() } return tempURL, nil } func (d *driver) swiftPath(path string) string { return strings.TrimLeft(strings.TrimRight(d.Prefix+"/files"+path, "/"), "/") } func (d *driver) swiftSegmentPath(path string) (string, error) { checksum := sha1.New() random := make([]byte, 32) if _, err := rand.Read(random); err != nil { return "", err } path = hex.EncodeToString(checksum.Sum(append([]byte(path), random...))) return strings.TrimLeft(strings.TrimRight(d.Prefix+"/segments/"+path[0:3]+"/"+path[3:], "/"), "/"), nil } func (d *driver) getAllSegments(path string) ([]swift.Object, error) { //a simple container listing works 99.9% of the time segments, err := d.Conn.ObjectsAll(d.Container, &swift.ObjectsOpts{Prefix: path}) if err != nil { if err == swift.ContainerNotFound { return nil, storagedriver.PathNotFoundError{Path: path} } return nil, err } //build a lookup table by object name hasObjectName := make(map[string]struct{}) for _, segment := range segments { hasObjectName[segment.Name] = struct{}{} } //The container listing might be outdated (i.e. not contain all existing //segment objects yet) because of temporary inconsistency (Swift is only //eventually consistent!). Check its completeness. segmentNumber := 0 for { segmentNumber++ segmentPath := getSegmentPath(path, segmentNumber) if _, seen := hasObjectName[segmentPath]; seen { continue } //This segment is missing in the container listing. Use a more reliable //request to check its existence. (HEAD requests on segments are //guaranteed to return the correct metadata, except for the pathological //case of an outage of large parts of the Swift cluster or its network, //since every segment is only written once.) segment, _, err := d.Conn.Object(d.Container, segmentPath) switch err { case nil: //found new segment -> keep going, more might be missing segments = append(segments, segment) continue case swift.ObjectNotFound: //This segment is missing. Since we upload segments sequentially, //there won't be any more segments after it. return segments, nil default: return nil, err //unexpected error } } } func (d *driver) createManifest(path string, segments string) error { headers := make(swift.Headers) headers["X-Object-Manifest"] = segments manifest, err := d.Conn.ObjectCreate(d.Container, d.swiftPath(path), false, "", contentType, headers) if err != nil { if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } if err := manifest.Close(); err != nil { if err == swift.ObjectNotFound { return storagedriver.PathNotFoundError{Path: path} } return err } return nil } func chunkFilenames(slice []string, maxSize int) (chunks [][]string, err error) { if maxSize > 0 { for offset := 0; offset < len(slice); offset += maxSize { chunkSize := maxSize if offset+chunkSize > len(slice) { chunkSize = len(slice) - offset } chunks = append(chunks, slice[offset:offset+chunkSize]) } } else { return nil, fmt.Errorf("Max chunk size must be > 0") } return } func parseManifest(manifest string) (container string, prefix string) { components := strings.SplitN(manifest, "/", 2) container = components[0] if len(components) > 1 { prefix = components[1] } return container, prefix } func generateSecret() (string, error) { var secretBytes [32]byte if _, err := rand.Read(secretBytes[:]); err != nil { return "", fmt.Errorf("could not generate random bytes for Swift secret key: %v", err) } return hex.EncodeToString(secretBytes[:]), nil } func getSegmentPath(segmentsPath string, partNumber int) string { return fmt.Sprintf("%s/%016d", segmentsPath, partNumber) } type writer struct { driver *driver path string segmentsPath string size int64 bw *bufio.Writer closed bool committed bool cancelled bool } func (d *driver) newWriter(path, segmentsPath string, segments []swift.Object) storagedriver.FileWriter { var size int64 for _, segment := range segments { size += segment.Bytes } return &writer{ driver: d, path: path, segmentsPath: segmentsPath, size: size, bw: bufio.NewWriterSize(&segmentWriter{ conn: d.Conn, container: d.Container, segmentsPath: segmentsPath, segmentNumber: len(segments) + 1, maxChunkSize: d.ChunkSize, }, d.ChunkSize), } } func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, fmt.Errorf("already closed") } else if w.committed { return 0, fmt.Errorf("already committed") } else if w.cancelled { return 0, fmt.Errorf("already cancelled") } n, err := w.bw.Write(p) w.size += int64(n) return n, err } func (w *writer) Size() int64 { return w.size } func (w *writer) Close() error { if w.closed { return fmt.Errorf("already closed") } if err := w.bw.Flush(); err != nil { return err } if !w.committed && !w.cancelled { if err := w.driver.createManifest(w.path, w.driver.Container+"/"+w.segmentsPath); err != nil { return err } if err := w.waitForSegmentsToShowUp(); err != nil { return err } } w.closed = true return nil } func (w *writer) Cancel() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } w.cancelled = true return w.driver.Delete(context.Background(), w.path) } func (w *writer) Commit() error { if w.closed { return fmt.Errorf("already closed") } else if w.committed { return fmt.Errorf("already committed") } else if w.cancelled { return fmt.Errorf("already cancelled") } if err := w.bw.Flush(); err != nil { return err } if err := w.driver.createManifest(w.path, w.driver.Container+"/"+w.segmentsPath); err != nil { return err } w.committed = true return w.waitForSegmentsToShowUp() } func (w *writer) waitForSegmentsToShowUp() error { var err error waitingTime := readAfterWriteWait endTime := time.Now().Add(readAfterWriteTimeout) for { var info swift.Object if info, _, err = w.driver.Conn.Object(w.driver.Container, w.driver.swiftPath(w.path)); err == nil { if info.Bytes == w.size { break } err = fmt.Errorf("Timeout expired while waiting for segments of %s to show up", w.path) } if time.Now().Add(waitingTime).After(endTime) { break } time.Sleep(waitingTime) waitingTime *= 2 } return err } type segmentWriter struct { conn *swift.Connection container string segmentsPath string segmentNumber int maxChunkSize int } func (sw *segmentWriter) Write(p []byte) (int, error) { n := 0 for offset := 0; offset < len(p); offset += sw.maxChunkSize { chunkSize := sw.maxChunkSize if offset+chunkSize > len(p) { chunkSize = len(p) - offset } _, err := sw.conn.ObjectPut(sw.container, getSegmentPath(sw.segmentsPath, sw.segmentNumber), bytes.NewReader(p[offset:offset+chunkSize]), false, "", contentType, nil) if err != nil { return n, err } sw.segmentNumber++ n += chunkSize } return n, nil } docker-registry-2.6.2~ds1/registry/storage/driver/swift/swift_test.go000066400000000000000000000135221313450123100261250ustar00rootroot00000000000000package swift import ( "io/ioutil" "os" "reflect" "strconv" "strings" "testing" "github.com/ncw/swift/swifttest" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/testsuites" "gopkg.in/check.v1" ) // Hook up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } var swiftDriverConstructor func(prefix string) (*Driver, error) func init() { var ( username string password string authURL string tenant string tenantID string domain string domainID string tenantDomain string tenantDomainID string trustID string container string region string AuthVersion int endpointType string insecureSkipVerify bool secretKey string accessKey string containerKey bool tempURLMethods []string swiftServer *swifttest.SwiftServer err error ) username = os.Getenv("SWIFT_USERNAME") password = os.Getenv("SWIFT_PASSWORD") authURL = os.Getenv("SWIFT_AUTH_URL") tenant = os.Getenv("SWIFT_TENANT_NAME") tenantID = os.Getenv("SWIFT_TENANT_ID") domain = os.Getenv("SWIFT_DOMAIN_NAME") domainID = os.Getenv("SWIFT_DOMAIN_ID") tenantDomain = os.Getenv("SWIFT_DOMAIN_NAME") tenantDomainID = os.Getenv("SWIFT_DOMAIN_ID") trustID = os.Getenv("SWIFT_TRUST_ID") container = os.Getenv("SWIFT_CONTAINER_NAME") region = os.Getenv("SWIFT_REGION_NAME") AuthVersion, _ = strconv.Atoi(os.Getenv("SWIFT_AUTH_VERSION")) endpointType = os.Getenv("SWIFT_ENDPOINT_TYPE") insecureSkipVerify, _ = strconv.ParseBool(os.Getenv("SWIFT_INSECURESKIPVERIFY")) secretKey = os.Getenv("SWIFT_SECRET_KEY") accessKey = os.Getenv("SWIFT_ACCESS_KEY") containerKey, _ = strconv.ParseBool(os.Getenv("SWIFT_TEMPURL_CONTAINERKEY")) tempURLMethods = strings.Split(os.Getenv("SWIFT_TEMPURL_METHODS"), ",") if username == "" || password == "" || authURL == "" || container == "" { if swiftServer, err = swifttest.NewSwiftServer("localhost"); err != nil { panic(err) } username = "swifttest" password = "swifttest" authURL = swiftServer.AuthURL container = "test" } prefix, err := ioutil.TempDir("", "driver-") if err != nil { panic(err) } defer os.Remove(prefix) swiftDriverConstructor = func(root string) (*Driver, error) { parameters := Parameters{ username, password, authURL, tenant, tenantID, domain, domainID, tenantDomain, tenantDomainID, trustID, region, AuthVersion, container, root, endpointType, insecureSkipVerify, defaultChunkSize, secretKey, accessKey, containerKey, tempURLMethods, } return New(parameters) } driverConstructor := func() (storagedriver.StorageDriver, error) { return swiftDriverConstructor(prefix) } testsuites.RegisterSuite(driverConstructor, testsuites.NeverSkip) } func TestEmptyRootList(t *testing.T) { validRoot, err := ioutil.TempDir("", "driver-") if err != nil { t.Fatalf("unexpected error creating temporary directory: %v", err) } defer os.Remove(validRoot) rootedDriver, err := swiftDriverConstructor(validRoot) if err != nil { t.Fatalf("unexpected error creating rooted driver: %v", err) } emptyRootDriver, err := swiftDriverConstructor("") if err != nil { t.Fatalf("unexpected error creating empty root driver: %v", err) } slashRootDriver, err := swiftDriverConstructor("/") if err != nil { t.Fatalf("unexpected error creating slash root driver: %v", err) } filename := "/test" contents := []byte("contents") ctx := context.Background() err = rootedDriver.PutContent(ctx, filename, contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } keys, err := emptyRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } keys, err = slashRootDriver.List(ctx, "/") for _, path := range keys { if !storagedriver.PathRegexp.MatchString(path) { t.Fatalf("unexpected string in path: %q != %q", path, storagedriver.PathRegexp) } } // Create an object with a path nested under the existing object err = rootedDriver.PutContent(ctx, filename+"/file1", contents) if err != nil { t.Fatalf("unexpected error creating content: %v", err) } err = rootedDriver.Delete(ctx, filename) if err != nil { t.Fatalf("failed to delete: %v", err) } keys, err = rootedDriver.List(ctx, "/") if err != nil { t.Fatalf("failed to list objects after deletion: %v", err) } if len(keys) != 0 { t.Fatal("delete did not remove nested objects") } } func TestFilenameChunking(t *testing.T) { // Test valid input and sizes input := []string{"a", "b", "c", "d", "e"} expecteds := [][][]string{ { {"a"}, {"b"}, {"c"}, {"d"}, {"e"}, }, { {"a", "b"}, {"c", "d"}, {"e"}, }, { {"a", "b", "c"}, {"d", "e"}, }, { {"a", "b", "c", "d"}, {"e"}, }, { {"a", "b", "c", "d", "e"}, }, { {"a", "b", "c", "d", "e"}, }, } for i, expected := range expecteds { actual, err := chunkFilenames(input, i+1) if !reflect.DeepEqual(actual, expected) { t.Fatalf("chunk %v didn't match expected value %v", actual, expected) } if err != nil { t.Fatalf("unexpected error chunking filenames: %v", err) } } // Test nil input actual, err := chunkFilenames(nil, 5) if len(actual) != 0 { t.Fatal("chunks were returned when passed nil") } if err != nil { t.Fatalf("unexpected error chunking filenames: %v", err) } // Test 0 and < 0 sizes actual, err = chunkFilenames(nil, 0) if err == nil { t.Fatal("expected error for size = 0") } actual, err = chunkFilenames(nil, -1) if err == nil { t.Fatal("expected error for size = -1") } } docker-registry-2.6.2~ds1/registry/storage/driver/testdriver/000077500000000000000000000000001313450123100244375ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/testdriver/testdriver.go000066400000000000000000000042601313450123100271630ustar00rootroot00000000000000package testdriver import ( "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/factory" "github.com/docker/distribution/registry/storage/driver/inmemory" ) const driverName = "testdriver" func init() { factory.Register(driverName, &testDriverFactory{}) } // testDriverFactory implements the factory.StorageDriverFactory interface. type testDriverFactory struct{} func (factory *testDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { return New(), nil } // TestDriver is a StorageDriver for testing purposes. The Writer returned by this driver // simulates the case where Write operations are buffered. This causes the value returned by Size to lag // behind until Close (or Commit, or Cancel) is called. type TestDriver struct { storagedriver.StorageDriver } type testFileWriter struct { storagedriver.FileWriter prevchunk []byte } var _ storagedriver.StorageDriver = &TestDriver{} // New constructs a new StorageDriver for testing purposes. The Writer returned by this driver // simulates the case where Write operations are buffered. This causes the value returned by Size to lag // behind until Close (or Commit, or Cancel) is called. func New() *TestDriver { return &TestDriver{StorageDriver: inmemory.New()} } // Writer returns a FileWriter which will store the content written to it // at the location designated by "path" after the call to Commit. func (td *TestDriver) Writer(ctx context.Context, path string, append bool) (storagedriver.FileWriter, error) { fw, err := td.StorageDriver.Writer(ctx, path, append) return &testFileWriter{FileWriter: fw}, err } func (tfw *testFileWriter) Write(p []byte) (int, error) { _, err := tfw.FileWriter.Write(tfw.prevchunk) tfw.prevchunk = make([]byte, len(p)) copy(tfw.prevchunk, p) return len(p), err } func (tfw *testFileWriter) Close() error { tfw.Write(nil) return tfw.FileWriter.Close() } func (tfw *testFileWriter) Cancel() error { tfw.Write(nil) return tfw.FileWriter.Cancel() } func (tfw *testFileWriter) Commit() error { tfw.Write(nil) return tfw.FileWriter.Commit() } docker-registry-2.6.2~ds1/registry/storage/driver/testsuites/000077500000000000000000000000001313450123100244605ustar00rootroot00000000000000docker-registry-2.6.2~ds1/registry/storage/driver/testsuites/testsuites.go000066400000000000000000001202151313450123100272240ustar00rootroot00000000000000package testsuites import ( "bytes" "crypto/sha1" "io" "io/ioutil" "math/rand" "net/http" "os" "path" "sort" "strings" "sync" "testing" "time" "gopkg.in/check.v1" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" ) // Test hooks up gocheck into the "go test" runner. func Test(t *testing.T) { check.TestingT(t) } // RegisterSuite registers an in-process storage driver test suite with // the go test runner. func RegisterSuite(driverConstructor DriverConstructor, skipCheck SkipCheck) { check.Suite(&DriverSuite{ Constructor: driverConstructor, SkipCheck: skipCheck, ctx: context.Background(), }) } // SkipCheck is a function used to determine if a test suite should be skipped. // If a SkipCheck returns a non-empty skip reason, the suite is skipped with // the given reason. type SkipCheck func() (reason string) // NeverSkip is a default SkipCheck which never skips the suite. var NeverSkip SkipCheck = func() string { return "" } // DriverConstructor is a function which returns a new // storagedriver.StorageDriver. type DriverConstructor func() (storagedriver.StorageDriver, error) // DriverTeardown is a function which cleans up a suite's // storagedriver.StorageDriver. type DriverTeardown func() error // DriverSuite is a gocheck test suite designed to test a // storagedriver.StorageDriver. The intended way to create a DriverSuite is // with RegisterSuite. type DriverSuite struct { Constructor DriverConstructor Teardown DriverTeardown SkipCheck storagedriver.StorageDriver ctx context.Context } // SetUpSuite sets up the gocheck test suite. func (suite *DriverSuite) SetUpSuite(c *check.C) { if reason := suite.SkipCheck(); reason != "" { c.Skip(reason) } d, err := suite.Constructor() c.Assert(err, check.IsNil) suite.StorageDriver = d } // TearDownSuite tears down the gocheck test suite. func (suite *DriverSuite) TearDownSuite(c *check.C) { if suite.Teardown != nil { err := suite.Teardown() c.Assert(err, check.IsNil) } } // TearDownTest tears down the gocheck test. // This causes the suite to abort if any files are left around in the storage // driver. func (suite *DriverSuite) TearDownTest(c *check.C) { files, _ := suite.StorageDriver.List(suite.ctx, "/") if len(files) > 0 { c.Fatalf("Storage driver did not clean up properly. Offending files: %#v", files) } } // TestRootExists ensures that all storage drivers have a root path by default. func (suite *DriverSuite) TestRootExists(c *check.C) { _, err := suite.StorageDriver.List(suite.ctx, "/") if err != nil { c.Fatalf(`the root path "/" should always exist: %v`, err) } } // TestValidPaths checks that various valid file paths are accepted by the // storage driver. func (suite *DriverSuite) TestValidPaths(c *check.C) { contents := randomContents(64) validFiles := []string{ "/a", "/2", "/aa", "/a.a", "/0-9/abcdefg", "/abcdefg/z.75", "/abc/1.2.3.4.5-6_zyx/123.z/4", "/docker/docker-registry", "/123.abc", "/abc./abc", "/.abc", "/a--b", "/a-.b", "/_.abc", "/Docker/docker-registry", "/Abc/Cba"} for _, filename := range validFiles { err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) defer suite.deletePath(c, firstPart(filename)) c.Assert(err, check.IsNil) received, err := suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.IsNil) c.Assert(received, check.DeepEquals, contents) } } func (suite *DriverSuite) deletePath(c *check.C, path string) { for tries := 2; tries > 0; tries-- { err := suite.StorageDriver.Delete(suite.ctx, path) if _, ok := err.(storagedriver.PathNotFoundError); ok { err = nil } c.Assert(err, check.IsNil) paths, err := suite.StorageDriver.List(suite.ctx, path) if len(paths) == 0 { break } time.Sleep(time.Second * 2) } } // TestInvalidPaths checks that various invalid file paths are rejected by the // storage driver. func (suite *DriverSuite) TestInvalidPaths(c *check.C) { contents := randomContents(64) invalidFiles := []string{ "", "/", "abc", "123.abc", "//bcd", "/abc_123/"} for _, filename := range invalidFiles { err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) // only delete if file was successfully written if err == nil { defer suite.deletePath(c, firstPart(filename)) } c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.InvalidPathError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.InvalidPathError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } } // TestWriteRead1 tests a simple write-read workflow. func (suite *DriverSuite) TestWriteRead1(c *check.C) { filename := randomPath(32) contents := []byte("a") suite.writeReadCompare(c, filename, contents) } // TestWriteRead2 tests a simple write-read workflow with unicode data. func (suite *DriverSuite) TestWriteRead2(c *check.C) { filename := randomPath(32) contents := []byte("\xc3\x9f") suite.writeReadCompare(c, filename, contents) } // TestWriteRead3 tests a simple write-read workflow with a small string. func (suite *DriverSuite) TestWriteRead3(c *check.C) { filename := randomPath(32) contents := randomContents(32) suite.writeReadCompare(c, filename, contents) } // TestWriteRead4 tests a simple write-read workflow with 1MB of data. func (suite *DriverSuite) TestWriteRead4(c *check.C) { filename := randomPath(32) contents := randomContents(1024 * 1024) suite.writeReadCompare(c, filename, contents) } // TestWriteReadNonUTF8 tests that non-utf8 data may be written to the storage // driver safely. func (suite *DriverSuite) TestWriteReadNonUTF8(c *check.C) { filename := randomPath(32) contents := []byte{0x80, 0x80, 0x80, 0x80} suite.writeReadCompare(c, filename, contents) } // TestTruncate tests that putting smaller contents than an original file does // remove the excess contents. func (suite *DriverSuite) TestTruncate(c *check.C) { filename := randomPath(32) contents := randomContents(1024 * 1024) suite.writeReadCompare(c, filename, contents) contents = randomContents(1024) suite.writeReadCompare(c, filename, contents) } // TestReadNonexistent tests reading content from an empty path. func (suite *DriverSuite) TestReadNonexistent(c *check.C) { filename := randomPath(32) _, err := suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestWriteReadStreams1 tests a simple write-read streaming workflow. func (suite *DriverSuite) TestWriteReadStreams1(c *check.C) { filename := randomPath(32) contents := []byte("a") suite.writeReadCompareStreams(c, filename, contents) } // TestWriteReadStreams2 tests a simple write-read streaming workflow with // unicode data. func (suite *DriverSuite) TestWriteReadStreams2(c *check.C) { filename := randomPath(32) contents := []byte("\xc3\x9f") suite.writeReadCompareStreams(c, filename, contents) } // TestWriteReadStreams3 tests a simple write-read streaming workflow with a // small amount of data. func (suite *DriverSuite) TestWriteReadStreams3(c *check.C) { filename := randomPath(32) contents := randomContents(32) suite.writeReadCompareStreams(c, filename, contents) } // TestWriteReadStreams4 tests a simple write-read streaming workflow with 1MB // of data. func (suite *DriverSuite) TestWriteReadStreams4(c *check.C) { filename := randomPath(32) contents := randomContents(1024 * 1024) suite.writeReadCompareStreams(c, filename, contents) } // TestWriteReadStreamsNonUTF8 tests that non-utf8 data may be written to the // storage driver safely. func (suite *DriverSuite) TestWriteReadStreamsNonUTF8(c *check.C) { filename := randomPath(32) contents := []byte{0x80, 0x80, 0x80, 0x80} suite.writeReadCompareStreams(c, filename, contents) } // TestWriteReadLargeStreams tests that a 5GB file may be written to the storage // driver safely. func (suite *DriverSuite) TestWriteReadLargeStreams(c *check.C) { if testing.Short() { c.Skip("Skipping test in short mode") } filename := randomPath(32) defer suite.deletePath(c, firstPart(filename)) checksum := sha1.New() var fileSize int64 = 5 * 1024 * 1024 * 1024 contents := newRandReader(fileSize) writer, err := suite.StorageDriver.Writer(suite.ctx, filename, false) c.Assert(err, check.IsNil) written, err := io.Copy(writer, io.TeeReader(contents, checksum)) c.Assert(err, check.IsNil) c.Assert(written, check.Equals, fileSize) err = writer.Commit() c.Assert(err, check.IsNil) err = writer.Close() c.Assert(err, check.IsNil) reader, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.IsNil) defer reader.Close() writtenChecksum := sha1.New() io.Copy(writtenChecksum, reader) c.Assert(writtenChecksum.Sum(nil), check.DeepEquals, checksum.Sum(nil)) } // TestReaderWithOffset tests that the appropriate data is streamed when // reading with a given offset. func (suite *DriverSuite) TestReaderWithOffset(c *check.C) { filename := randomPath(32) defer suite.deletePath(c, firstPart(filename)) chunkSize := int64(32) contentsChunk1 := randomContents(chunkSize) contentsChunk2 := randomContents(chunkSize) contentsChunk3 := randomContents(chunkSize) err := suite.StorageDriver.PutContent(suite.ctx, filename, append(append(contentsChunk1, contentsChunk2...), contentsChunk3...)) c.Assert(err, check.IsNil) reader, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.IsNil) defer reader.Close() readContents, err := ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, append(append(contentsChunk1, contentsChunk2...), contentsChunk3...)) reader, err = suite.StorageDriver.Reader(suite.ctx, filename, chunkSize) c.Assert(err, check.IsNil) defer reader.Close() readContents, err = ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, append(contentsChunk2, contentsChunk3...)) reader, err = suite.StorageDriver.Reader(suite.ctx, filename, chunkSize*2) c.Assert(err, check.IsNil) defer reader.Close() readContents, err = ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contentsChunk3) // Ensure we get invalid offest for negative offsets. reader, err = suite.StorageDriver.Reader(suite.ctx, filename, -1) c.Assert(err, check.FitsTypeOf, storagedriver.InvalidOffsetError{}) c.Assert(err.(storagedriver.InvalidOffsetError).Offset, check.Equals, int64(-1)) c.Assert(err.(storagedriver.InvalidOffsetError).Path, check.Equals, filename) c.Assert(reader, check.IsNil) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) // Read past the end of the content and make sure we get a reader that // returns 0 bytes and io.EOF reader, err = suite.StorageDriver.Reader(suite.ctx, filename, chunkSize*3) c.Assert(err, check.IsNil) defer reader.Close() buf := make([]byte, chunkSize) n, err := reader.Read(buf) c.Assert(err, check.Equals, io.EOF) c.Assert(n, check.Equals, 0) // Check the N-1 boundary condition, ensuring we get 1 byte then io.EOF. reader, err = suite.StorageDriver.Reader(suite.ctx, filename, chunkSize*3-1) c.Assert(err, check.IsNil) defer reader.Close() n, err = reader.Read(buf) c.Assert(n, check.Equals, 1) // We don't care whether the io.EOF comes on the this read or the first // zero read, but the only error acceptable here is io.EOF. if err != nil { c.Assert(err, check.Equals, io.EOF) } // Any more reads should result in zero bytes and io.EOF n, err = reader.Read(buf) c.Assert(n, check.Equals, 0) c.Assert(err, check.Equals, io.EOF) } // TestContinueStreamAppendLarge tests that a stream write can be appended to without // corrupting the data with a large chunk size. func (suite *DriverSuite) TestContinueStreamAppendLarge(c *check.C) { suite.testContinueStreamAppend(c, int64(10*1024*1024)) } // TestContinueStreamAppendSmall is the same as TestContinueStreamAppendLarge, but only // with a tiny chunk size in order to test corner cases for some cloud storage drivers. func (suite *DriverSuite) TestContinueStreamAppendSmall(c *check.C) { suite.testContinueStreamAppend(c, int64(32)) } func (suite *DriverSuite) testContinueStreamAppend(c *check.C, chunkSize int64) { filename := randomPath(32) defer suite.deletePath(c, firstPart(filename)) contentsChunk1 := randomContents(chunkSize) contentsChunk2 := randomContents(chunkSize) contentsChunk3 := randomContents(chunkSize) fullContents := append(append(contentsChunk1, contentsChunk2...), contentsChunk3...) writer, err := suite.StorageDriver.Writer(suite.ctx, filename, false) c.Assert(err, check.IsNil) nn, err := io.Copy(writer, bytes.NewReader(contentsChunk1)) c.Assert(err, check.IsNil) c.Assert(nn, check.Equals, int64(len(contentsChunk1))) err = writer.Close() c.Assert(err, check.IsNil) curSize := writer.Size() c.Assert(curSize, check.Equals, int64(len(contentsChunk1))) writer, err = suite.StorageDriver.Writer(suite.ctx, filename, true) c.Assert(err, check.IsNil) c.Assert(writer.Size(), check.Equals, curSize) nn, err = io.Copy(writer, bytes.NewReader(contentsChunk2)) c.Assert(err, check.IsNil) c.Assert(nn, check.Equals, int64(len(contentsChunk2))) err = writer.Close() c.Assert(err, check.IsNil) curSize = writer.Size() c.Assert(curSize, check.Equals, 2*chunkSize) writer, err = suite.StorageDriver.Writer(suite.ctx, filename, true) c.Assert(err, check.IsNil) c.Assert(writer.Size(), check.Equals, curSize) nn, err = io.Copy(writer, bytes.NewReader(fullContents[curSize:])) c.Assert(err, check.IsNil) c.Assert(nn, check.Equals, int64(len(fullContents[curSize:]))) err = writer.Commit() c.Assert(err, check.IsNil) err = writer.Close() c.Assert(err, check.IsNil) received, err := suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.IsNil) c.Assert(received, check.DeepEquals, fullContents) } // TestReadNonexistentStream tests that reading a stream for a nonexistent path // fails. func (suite *DriverSuite) TestReadNonexistentStream(c *check.C) { filename := randomPath(32) _, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.Reader(suite.ctx, filename, 64) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestList checks the returned list of keys after populating a directory tree. func (suite *DriverSuite) TestList(c *check.C) { rootDirectory := "/" + randomFilename(int64(8+rand.Intn(8))) defer suite.deletePath(c, rootDirectory) doesnotexist := path.Join(rootDirectory, "nonexistent") _, err := suite.StorageDriver.List(suite.ctx, doesnotexist) c.Assert(err, check.Equals, storagedriver.PathNotFoundError{ Path: doesnotexist, DriverName: suite.StorageDriver.Name(), }) parentDirectory := rootDirectory + "/" + randomFilename(int64(8+rand.Intn(8))) childFiles := make([]string, 50) for i := 0; i < len(childFiles); i++ { childFile := parentDirectory + "/" + randomFilename(int64(8+rand.Intn(8))) childFiles[i] = childFile err := suite.StorageDriver.PutContent(suite.ctx, childFile, randomContents(32)) c.Assert(err, check.IsNil) } sort.Strings(childFiles) keys, err := suite.StorageDriver.List(suite.ctx, "/") c.Assert(err, check.IsNil) c.Assert(keys, check.DeepEquals, []string{rootDirectory}) keys, err = suite.StorageDriver.List(suite.ctx, rootDirectory) c.Assert(err, check.IsNil) c.Assert(keys, check.DeepEquals, []string{parentDirectory}) keys, err = suite.StorageDriver.List(suite.ctx, parentDirectory) c.Assert(err, check.IsNil) sort.Strings(keys) c.Assert(keys, check.DeepEquals, childFiles) // A few checks to add here (check out #819 for more discussion on this): // 1. Ensure that all paths are absolute. // 2. Ensure that listings only include direct children. // 3. Ensure that we only respond to directory listings that end with a slash (maybe?). } // TestMove checks that a moved object no longer exists at the source path and // does exist at the destination. func (suite *DriverSuite) TestMove(c *check.C) { contents := randomContents(32) sourcePath := randomPath(32) destPath := randomPath(32) defer suite.deletePath(c, firstPart(sourcePath)) defer suite.deletePath(c, firstPart(destPath)) err := suite.StorageDriver.PutContent(suite.ctx, sourcePath, contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Move(suite.ctx, sourcePath, destPath) c.Assert(err, check.IsNil) received, err := suite.StorageDriver.GetContent(suite.ctx, destPath) c.Assert(err, check.IsNil) c.Assert(received, check.DeepEquals, contents) _, err = suite.StorageDriver.GetContent(suite.ctx, sourcePath) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestMoveOverwrite checks that a moved object no longer exists at the source // path and overwrites the contents at the destination. func (suite *DriverSuite) TestMoveOverwrite(c *check.C) { sourcePath := randomPath(32) destPath := randomPath(32) sourceContents := randomContents(32) destContents := randomContents(64) defer suite.deletePath(c, firstPart(sourcePath)) defer suite.deletePath(c, firstPart(destPath)) err := suite.StorageDriver.PutContent(suite.ctx, sourcePath, sourceContents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, destPath, destContents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Move(suite.ctx, sourcePath, destPath) c.Assert(err, check.IsNil) received, err := suite.StorageDriver.GetContent(suite.ctx, destPath) c.Assert(err, check.IsNil) c.Assert(received, check.DeepEquals, sourceContents) _, err = suite.StorageDriver.GetContent(suite.ctx, sourcePath) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestMoveNonexistent checks that moving a nonexistent key fails and does not // delete the data at the destination path. func (suite *DriverSuite) TestMoveNonexistent(c *check.C) { contents := randomContents(32) sourcePath := randomPath(32) destPath := randomPath(32) defer suite.deletePath(c, firstPart(destPath)) err := suite.StorageDriver.PutContent(suite.ctx, destPath, contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Move(suite.ctx, sourcePath, destPath) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) received, err := suite.StorageDriver.GetContent(suite.ctx, destPath) c.Assert(err, check.IsNil) c.Assert(received, check.DeepEquals, contents) } // TestMoveInvalid provides various checks for invalid moves. func (suite *DriverSuite) TestMoveInvalid(c *check.C) { contents := randomContents(32) // Create a regular file. err := suite.StorageDriver.PutContent(suite.ctx, "/notadir", contents) c.Assert(err, check.IsNil) defer suite.deletePath(c, "/notadir") // Now try to move a non-existent file under it. err = suite.StorageDriver.Move(suite.ctx, "/notadir/foo", "/notadir/bar") c.Assert(err, check.NotNil) // non-nil error } // TestDelete checks that the delete operation removes data from the storage // driver func (suite *DriverSuite) TestDelete(c *check.C) { filename := randomPath(32) contents := randomContents(32) defer suite.deletePath(c, firstPart(filename)) err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Delete(suite.ctx, filename) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestURLFor checks that the URLFor method functions properly, but only if it // is implemented func (suite *DriverSuite) TestURLFor(c *check.C) { filename := randomPath(32) contents := randomContents(32) defer suite.deletePath(c, firstPart(filename)) err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) url, err := suite.StorageDriver.URLFor(suite.ctx, filename, nil) if _, ok := err.(storagedriver.ErrUnsupportedMethod); ok { return } c.Assert(err, check.IsNil) response, err := http.Get(url) c.Assert(err, check.IsNil) defer response.Body.Close() read, err := ioutil.ReadAll(response.Body) c.Assert(err, check.IsNil) c.Assert(read, check.DeepEquals, contents) url, err = suite.StorageDriver.URLFor(suite.ctx, filename, map[string]interface{}{"method": "HEAD"}) if _, ok := err.(storagedriver.ErrUnsupportedMethod); ok { return } c.Assert(err, check.IsNil) response, err = http.Head(url) c.Assert(response.StatusCode, check.Equals, 200) c.Assert(response.ContentLength, check.Equals, int64(32)) } // TestDeleteNonexistent checks that removing a nonexistent key fails. func (suite *DriverSuite) TestDeleteNonexistent(c *check.C) { filename := randomPath(32) err := suite.StorageDriver.Delete(suite.ctx, filename) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestDeleteFolder checks that deleting a folder removes all child elements. func (suite *DriverSuite) TestDeleteFolder(c *check.C) { dirname := randomPath(32) filename1 := randomPath(32) filename2 := randomPath(32) filename3 := randomPath(32) contents := randomContents(32) defer suite.deletePath(c, firstPart(dirname)) err := suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, filename1), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, filename2), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, filename3), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Delete(suite.ctx, path.Join(dirname, filename1)) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename1)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename2)) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename3)) c.Assert(err, check.IsNil) err = suite.StorageDriver.Delete(suite.ctx, dirname) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename1)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename2)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename3)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) } // TestDeleteOnlyDeletesSubpaths checks that deleting path A does not // delete path B when A is a prefix of B but B is not a subpath of A (so that // deleting "/a" does not delete "/ab"). This matters for services like S3 that // do not implement directories. func (suite *DriverSuite) TestDeleteOnlyDeletesSubpaths(c *check.C) { dirname := randomPath(32) filename := randomPath(32) contents := randomContents(32) defer suite.deletePath(c, firstPart(dirname)) err := suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, filename), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, filename+"suffix"), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, dirname, filename), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, path.Join(dirname, dirname+"suffix", filename), contents) c.Assert(err, check.IsNil) err = suite.StorageDriver.Delete(suite.ctx, path.Join(dirname, filename)) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, filename+"suffix")) c.Assert(err, check.IsNil) err = suite.StorageDriver.Delete(suite.ctx, path.Join(dirname, dirname)) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, dirname, filename)) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) _, err = suite.StorageDriver.GetContent(suite.ctx, path.Join(dirname, dirname+"suffix", filename)) c.Assert(err, check.IsNil) } // TestStatCall runs verifies the implementation of the storagedriver's Stat call. func (suite *DriverSuite) TestStatCall(c *check.C) { content := randomContents(4096) dirPath := randomPath(32) fileName := randomFilename(32) filePath := path.Join(dirPath, fileName) defer suite.deletePath(c, firstPart(dirPath)) // Call on non-existent file/dir, check error. fi, err := suite.StorageDriver.Stat(suite.ctx, dirPath) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) c.Assert(fi, check.IsNil) fi, err = suite.StorageDriver.Stat(suite.ctx, filePath) c.Assert(err, check.NotNil) c.Assert(err, check.FitsTypeOf, storagedriver.PathNotFoundError{}) c.Assert(strings.Contains(err.Error(), suite.Name()), check.Equals, true) c.Assert(fi, check.IsNil) err = suite.StorageDriver.PutContent(suite.ctx, filePath, content) c.Assert(err, check.IsNil) // Call on regular file, check results fi, err = suite.StorageDriver.Stat(suite.ctx, filePath) c.Assert(err, check.IsNil) c.Assert(fi, check.NotNil) c.Assert(fi.Path(), check.Equals, filePath) c.Assert(fi.Size(), check.Equals, int64(len(content))) c.Assert(fi.IsDir(), check.Equals, false) createdTime := fi.ModTime() // Sleep and modify the file time.Sleep(time.Second * 10) content = randomContents(4096) err = suite.StorageDriver.PutContent(suite.ctx, filePath, content) c.Assert(err, check.IsNil) fi, err = suite.StorageDriver.Stat(suite.ctx, filePath) c.Assert(err, check.IsNil) c.Assert(fi, check.NotNil) time.Sleep(time.Second * 5) // allow changes to propagate (eventual consistency) // Check if the modification time is after the creation time. // In case of cloud storage services, storage frontend nodes might have // time drift between them, however that should be solved with sleeping // before update. modTime := fi.ModTime() if !modTime.After(createdTime) { c.Errorf("modtime (%s) is before the creation time (%s)", modTime, createdTime) } // Call on directory (do not check ModTime as dirs don't need to support it) fi, err = suite.StorageDriver.Stat(suite.ctx, dirPath) c.Assert(err, check.IsNil) c.Assert(fi, check.NotNil) c.Assert(fi.Path(), check.Equals, dirPath) c.Assert(fi.Size(), check.Equals, int64(0)) c.Assert(fi.IsDir(), check.Equals, true) } // TestPutContentMultipleTimes checks that if storage driver can overwrite the content // in the subsequent puts. Validates that PutContent does not have to work // with an offset like Writer does and overwrites the file entirely // rather than writing the data to the [0,len(data)) of the file. func (suite *DriverSuite) TestPutContentMultipleTimes(c *check.C) { filename := randomPath(32) contents := randomContents(4096) defer suite.deletePath(c, firstPart(filename)) err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) contents = randomContents(2048) // upload a different, smaller file err = suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) readContents, err := suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contents) } // TestConcurrentStreamReads checks that multiple clients can safely read from // the same file simultaneously with various offsets. func (suite *DriverSuite) TestConcurrentStreamReads(c *check.C) { var filesize int64 = 128 * 1024 * 1024 if testing.Short() { filesize = 10 * 1024 * 1024 c.Log("Reducing file size to 10MB for short mode") } filename := randomPath(32) contents := randomContents(filesize) defer suite.deletePath(c, firstPart(filename)) err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) var wg sync.WaitGroup readContents := func() { defer wg.Done() offset := rand.Int63n(int64(len(contents))) reader, err := suite.StorageDriver.Reader(suite.ctx, filename, offset) c.Assert(err, check.IsNil) readContents, err := ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contents[offset:]) } wg.Add(10) for i := 0; i < 10; i++ { go readContents() } wg.Wait() } // TestConcurrentFileStreams checks that multiple *os.File objects can be passed // in to Writer concurrently without hanging. func (suite *DriverSuite) TestConcurrentFileStreams(c *check.C) { numStreams := 32 if testing.Short() { numStreams = 8 c.Log("Reducing number of streams to 8 for short mode") } var wg sync.WaitGroup testStream := func(size int64) { defer wg.Done() suite.testFileStreams(c, size) } wg.Add(numStreams) for i := numStreams; i > 0; i-- { go testStream(int64(numStreams) * 1024 * 1024) } wg.Wait() } // TODO (brianbland): evaluate the relevancy of this test // TestEventualConsistency checks that if stat says that a file is a certain size, then // you can freely read from the file (this is the only guarantee that the driver needs to provide) // func (suite *DriverSuite) TestEventualConsistency(c *check.C) { // if testing.Short() { // c.Skip("Skipping test in short mode") // } // // filename := randomPath(32) // defer suite.deletePath(c, firstPart(filename)) // // var offset int64 // var misswrites int // var chunkSize int64 = 32 // // for i := 0; i < 1024; i++ { // contents := randomContents(chunkSize) // read, err := suite.StorageDriver.Writer(suite.ctx, filename, offset, bytes.NewReader(contents)) // c.Assert(err, check.IsNil) // // fi, err := suite.StorageDriver.Stat(suite.ctx, filename) // c.Assert(err, check.IsNil) // // // We are most concerned with being able to read data as soon as Stat declares // // it is uploaded. This is the strongest guarantee that some drivers (that guarantee // // at best eventual consistency) absolutely need to provide. // if fi.Size() == offset+chunkSize { // reader, err := suite.StorageDriver.Reader(suite.ctx, filename, offset) // c.Assert(err, check.IsNil) // // readContents, err := ioutil.ReadAll(reader) // c.Assert(err, check.IsNil) // // c.Assert(readContents, check.DeepEquals, contents) // // reader.Close() // offset += read // } else { // misswrites++ // } // } // // if misswrites > 0 { // c.Log("There were " + string(misswrites) + " occurrences of a write not being instantly available.") // } // // c.Assert(misswrites, check.Not(check.Equals), 1024) // } // BenchmarkPutGetEmptyFiles benchmarks PutContent/GetContent for 0B files func (suite *DriverSuite) BenchmarkPutGetEmptyFiles(c *check.C) { suite.benchmarkPutGetFiles(c, 0) } // BenchmarkPutGet1KBFiles benchmarks PutContent/GetContent for 1KB files func (suite *DriverSuite) BenchmarkPutGet1KBFiles(c *check.C) { suite.benchmarkPutGetFiles(c, 1024) } // BenchmarkPutGet1MBFiles benchmarks PutContent/GetContent for 1MB files func (suite *DriverSuite) BenchmarkPutGet1MBFiles(c *check.C) { suite.benchmarkPutGetFiles(c, 1024*1024) } // BenchmarkPutGet1GBFiles benchmarks PutContent/GetContent for 1GB files func (suite *DriverSuite) BenchmarkPutGet1GBFiles(c *check.C) { suite.benchmarkPutGetFiles(c, 1024*1024*1024) } func (suite *DriverSuite) benchmarkPutGetFiles(c *check.C, size int64) { c.SetBytes(size) parentDir := randomPath(8) defer func() { c.StopTimer() suite.StorageDriver.Delete(suite.ctx, firstPart(parentDir)) }() for i := 0; i < c.N; i++ { filename := path.Join(parentDir, randomPath(32)) err := suite.StorageDriver.PutContent(suite.ctx, filename, randomContents(size)) c.Assert(err, check.IsNil) _, err = suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.IsNil) } } // BenchmarkStreamEmptyFiles benchmarks Writer/Reader for 0B files func (suite *DriverSuite) BenchmarkStreamEmptyFiles(c *check.C) { suite.benchmarkStreamFiles(c, 0) } // BenchmarkStream1KBFiles benchmarks Writer/Reader for 1KB files func (suite *DriverSuite) BenchmarkStream1KBFiles(c *check.C) { suite.benchmarkStreamFiles(c, 1024) } // BenchmarkStream1MBFiles benchmarks Writer/Reader for 1MB files func (suite *DriverSuite) BenchmarkStream1MBFiles(c *check.C) { suite.benchmarkStreamFiles(c, 1024*1024) } // BenchmarkStream1GBFiles benchmarks Writer/Reader for 1GB files func (suite *DriverSuite) BenchmarkStream1GBFiles(c *check.C) { suite.benchmarkStreamFiles(c, 1024*1024*1024) } func (suite *DriverSuite) benchmarkStreamFiles(c *check.C, size int64) { c.SetBytes(size) parentDir := randomPath(8) defer func() { c.StopTimer() suite.StorageDriver.Delete(suite.ctx, firstPart(parentDir)) }() for i := 0; i < c.N; i++ { filename := path.Join(parentDir, randomPath(32)) writer, err := suite.StorageDriver.Writer(suite.ctx, filename, false) c.Assert(err, check.IsNil) written, err := io.Copy(writer, bytes.NewReader(randomContents(size))) c.Assert(err, check.IsNil) c.Assert(written, check.Equals, size) err = writer.Commit() c.Assert(err, check.IsNil) err = writer.Close() c.Assert(err, check.IsNil) rc, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.IsNil) rc.Close() } } // BenchmarkList5Files benchmarks List for 5 small files func (suite *DriverSuite) BenchmarkList5Files(c *check.C) { suite.benchmarkListFiles(c, 5) } // BenchmarkList50Files benchmarks List for 50 small files func (suite *DriverSuite) BenchmarkList50Files(c *check.C) { suite.benchmarkListFiles(c, 50) } func (suite *DriverSuite) benchmarkListFiles(c *check.C, numFiles int64) { parentDir := randomPath(8) defer func() { c.StopTimer() suite.StorageDriver.Delete(suite.ctx, firstPart(parentDir)) }() for i := int64(0); i < numFiles; i++ { err := suite.StorageDriver.PutContent(suite.ctx, path.Join(parentDir, randomPath(32)), nil) c.Assert(err, check.IsNil) } c.ResetTimer() for i := 0; i < c.N; i++ { files, err := suite.StorageDriver.List(suite.ctx, parentDir) c.Assert(err, check.IsNil) c.Assert(int64(len(files)), check.Equals, numFiles) } } // BenchmarkDelete5Files benchmarks Delete for 5 small files func (suite *DriverSuite) BenchmarkDelete5Files(c *check.C) { suite.benchmarkDeleteFiles(c, 5) } // BenchmarkDelete50Files benchmarks Delete for 50 small files func (suite *DriverSuite) BenchmarkDelete50Files(c *check.C) { suite.benchmarkDeleteFiles(c, 50) } func (suite *DriverSuite) benchmarkDeleteFiles(c *check.C, numFiles int64) { for i := 0; i < c.N; i++ { parentDir := randomPath(8) defer suite.deletePath(c, firstPart(parentDir)) c.StopTimer() for j := int64(0); j < numFiles; j++ { err := suite.StorageDriver.PutContent(suite.ctx, path.Join(parentDir, randomPath(32)), nil) c.Assert(err, check.IsNil) } c.StartTimer() // This is the operation we're benchmarking err := suite.StorageDriver.Delete(suite.ctx, firstPart(parentDir)) c.Assert(err, check.IsNil) } } func (suite *DriverSuite) testFileStreams(c *check.C, size int64) { tf, err := ioutil.TempFile("", "tf") c.Assert(err, check.IsNil) defer os.Remove(tf.Name()) defer tf.Close() filename := randomPath(32) defer suite.deletePath(c, firstPart(filename)) contents := randomContents(size) _, err = tf.Write(contents) c.Assert(err, check.IsNil) tf.Sync() tf.Seek(0, os.SEEK_SET) writer, err := suite.StorageDriver.Writer(suite.ctx, filename, false) c.Assert(err, check.IsNil) nn, err := io.Copy(writer, tf) c.Assert(err, check.IsNil) c.Assert(nn, check.Equals, size) err = writer.Commit() c.Assert(err, check.IsNil) err = writer.Close() c.Assert(err, check.IsNil) reader, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.IsNil) defer reader.Close() readContents, err := ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contents) } func (suite *DriverSuite) writeReadCompare(c *check.C, filename string, contents []byte) { defer suite.deletePath(c, firstPart(filename)) err := suite.StorageDriver.PutContent(suite.ctx, filename, contents) c.Assert(err, check.IsNil) readContents, err := suite.StorageDriver.GetContent(suite.ctx, filename) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contents) } func (suite *DriverSuite) writeReadCompareStreams(c *check.C, filename string, contents []byte) { defer suite.deletePath(c, firstPart(filename)) writer, err := suite.StorageDriver.Writer(suite.ctx, filename, false) c.Assert(err, check.IsNil) nn, err := io.Copy(writer, bytes.NewReader(contents)) c.Assert(err, check.IsNil) c.Assert(nn, check.Equals, int64(len(contents))) err = writer.Commit() c.Assert(err, check.IsNil) err = writer.Close() c.Assert(err, check.IsNil) reader, err := suite.StorageDriver.Reader(suite.ctx, filename, 0) c.Assert(err, check.IsNil) defer reader.Close() readContents, err := ioutil.ReadAll(reader) c.Assert(err, check.IsNil) c.Assert(readContents, check.DeepEquals, contents) } var filenameChars = []byte("abcdefghijklmnopqrstuvwxyz0123456789") var separatorChars = []byte("._-") func randomPath(length int64) string { path := "/" for int64(len(path)) < length { chunkLength := rand.Int63n(length-int64(len(path))) + 1 chunk := randomFilename(chunkLength) path += chunk remaining := length - int64(len(path)) if remaining == 1 { path += randomFilename(1) } else if remaining > 1 { path += "/" } } return path } func randomFilename(length int64) string { b := make([]byte, length) wasSeparator := true for i := range b { if !wasSeparator && i < len(b)-1 && rand.Intn(4) == 0 { b[i] = separatorChars[rand.Intn(len(separatorChars))] wasSeparator = true } else { b[i] = filenameChars[rand.Intn(len(filenameChars))] wasSeparator = false } } return string(b) } // randomBytes pre-allocates all of the memory sizes needed for the test. If // anything panics while accessing randomBytes, just make this number bigger. var randomBytes = make([]byte, 128<<20) func init() { _, _ = rand.Read(randomBytes) // always returns len(randomBytes) and nil error } func randomContents(length int64) []byte { return randomBytes[:length] } type randReader struct { r int64 m sync.Mutex } func (rr *randReader) Read(p []byte) (n int, err error) { rr.m.Lock() defer rr.m.Unlock() toread := int64(len(p)) if toread > rr.r { toread = rr.r } n = copy(p, randomContents(toread)) rr.r -= int64(n) if rr.r <= 0 { err = io.EOF } return } func newRandReader(n int64) *randReader { return &randReader{r: n} } func firstPart(filePath string) string { if filePath == "" { return "/" } for { if filePath[len(filePath)-1] == '/' { filePath = filePath[:len(filePath)-1] } dir, file := path.Split(filePath) if dir == "" && file == "" { return "/" } if dir == "/" || dir == "" { return "/" + file } if file == "" { return dir } filePath = dir } } docker-registry-2.6.2~ds1/registry/storage/filereader.go000066400000000000000000000077521313450123100234150ustar00rootroot00000000000000package storage import ( "bufio" "bytes" "fmt" "io" "io/ioutil" "os" "github.com/docker/distribution/context" storagedriver "github.com/docker/distribution/registry/storage/driver" ) // TODO(stevvooe): Set an optimal buffer size here. We'll have to // understand the latency characteristics of the underlying network to // set this correctly, so we may want to leave it to the driver. For // out of process drivers, we'll have to optimize this buffer size for // local communication. const fileReaderBufferSize = 4 << 20 // remoteFileReader provides a read seeker interface to files stored in // storagedriver. Used to implement part of layer interface and will be used // to implement read side of LayerUpload. type fileReader struct { driver storagedriver.StorageDriver ctx context.Context // identifying fields path string size int64 // size is the total size, must be set. // mutable fields rc io.ReadCloser // remote read closer brd *bufio.Reader // internal buffered io offset int64 // offset is the current read offset err error // terminal error, if set, reader is closed } // newFileReader initializes a file reader for the remote file. The reader // takes on the size and path that must be determined externally with a stat // call. The reader operates optimistically, assuming that the file is already // there. func newFileReader(ctx context.Context, driver storagedriver.StorageDriver, path string, size int64) (*fileReader, error) { return &fileReader{ ctx: ctx, driver: driver, path: path, size: size, }, nil } func (fr *fileReader) Read(p []byte) (n int, err error) { if fr.err != nil { return 0, fr.err } rd, err := fr.reader() if err != nil { return 0, err } n, err = rd.Read(p) fr.offset += int64(n) // Simulate io.EOR error if we reach filesize. if err == nil && fr.offset >= fr.size { err = io.EOF } return n, err } func (fr *fileReader) Seek(offset int64, whence int) (int64, error) { if fr.err != nil { return 0, fr.err } var err error newOffset := fr.offset switch whence { case os.SEEK_CUR: newOffset += int64(offset) case os.SEEK_END: newOffset = fr.size + int64(offset) case os.SEEK_SET: newOffset = int64(offset) } if newOffset < 0 { err = fmt.Errorf("cannot seek to negative position") } else { if fr.offset != newOffset { fr.reset() } // No problems, set the offset. fr.offset = newOffset } return fr.offset, err } func (fr *fileReader) Close() error { return fr.closeWithErr(fmt.Errorf("fileReader: closed")) } // reader prepares the current reader at the lrs offset, ensuring its buffered // and ready to go. func (fr *fileReader) reader() (io.Reader, error) { if fr.err != nil { return nil, fr.err } if fr.rc != nil { return fr.brd, nil } // If we don't have a reader, open one up. rc, err := fr.driver.Reader(fr.ctx, fr.path, fr.offset) if err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: // NOTE(stevvooe): If the path is not found, we simply return a // reader that returns io.EOF. However, we do not set fr.rc, // allowing future attempts at getting a reader to possibly // succeed if the file turns up later. return ioutil.NopCloser(bytes.NewReader([]byte{})), nil default: return nil, err } } fr.rc = rc if fr.brd == nil { fr.brd = bufio.NewReaderSize(fr.rc, fileReaderBufferSize) } else { fr.brd.Reset(fr.rc) } return fr.brd, nil } // resetReader resets the reader, forcing the read method to open up a new // connection and rebuild the buffered reader. This should be called when the // offset and the reader will become out of sync, such as during a seek // operation. func (fr *fileReader) reset() { if fr.err != nil { return } if fr.rc != nil { fr.rc.Close() fr.rc = nil } } func (fr *fileReader) closeWithErr(err error) error { if fr.err != nil { return fr.err } fr.err = err // close and release reader chain if fr.rc != nil { fr.rc.Close() } fr.rc = nil fr.brd = nil return fr.err } docker-registry-2.6.2~ds1/registry/storage/filereader_test.go000066400000000000000000000123401313450123100244410ustar00rootroot00000000000000package storage import ( "bytes" "io" mrand "math/rand" "os" "testing" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/storage/driver/inmemory" ) func TestSimpleRead(t *testing.T) { ctx := context.Background() content := make([]byte, 1<<20) n, err := mrand.Read(content) if err != nil { t.Fatalf("unexpected error building random data: %v", err) } if n != len(content) { t.Fatalf("random read didn't fill buffer") } dgst, err := digest.FromReader(bytes.NewReader(content)) if err != nil { t.Fatalf("unexpected error digesting random content: %v", err) } driver := inmemory.New() path := "/random" if err := driver.PutContent(ctx, path, content); err != nil { t.Fatalf("error putting patterned content: %v", err) } fr, err := newFileReader(ctx, driver, path, int64(len(content))) if err != nil { t.Fatalf("error allocating file reader: %v", err) } verifier, err := digest.NewDigestVerifier(dgst) if err != nil { t.Fatalf("error getting digest verifier: %s", err) } io.Copy(verifier, fr) if !verifier.Verified() { t.Fatalf("unable to verify read data") } } func TestFileReaderSeek(t *testing.T) { driver := inmemory.New() pattern := "01234567890ab" // prime length block repititions := 1024 path := "/patterned" content := bytes.Repeat([]byte(pattern), repititions) ctx := context.Background() if err := driver.PutContent(ctx, path, content); err != nil { t.Fatalf("error putting patterned content: %v", err) } fr, err := newFileReader(ctx, driver, path, int64(len(content))) if err != nil { t.Fatalf("unexpected error creating file reader: %v", err) } // Seek all over the place, in blocks of pattern size and make sure we get // the right data. for _, repitition := range mrand.Perm(repititions - 1) { targetOffset := int64(len(pattern) * repitition) // Seek to a multiple of pattern size and read pattern size bytes offset, err := fr.Seek(targetOffset, os.SEEK_SET) if err != nil { t.Fatalf("unexpected error seeking: %v", err) } if offset != targetOffset { t.Fatalf("did not seek to correct offset: %d != %d", offset, targetOffset) } p := make([]byte, len(pattern)) n, err := fr.Read(p) if err != nil { t.Fatalf("error reading pattern: %v", err) } if n != len(pattern) { t.Fatalf("incorrect read length: %d != %d", n, len(pattern)) } if string(p) != pattern { t.Fatalf("incorrect read content: %q != %q", p, pattern) } // Check offset current, err := fr.Seek(0, os.SEEK_CUR) if err != nil { t.Fatalf("error checking current offset: %v", err) } if current != targetOffset+int64(len(pattern)) { t.Fatalf("unexpected offset after read: %v", err) } } start, err := fr.Seek(0, os.SEEK_SET) if err != nil { t.Fatalf("error seeking to start: %v", err) } if start != 0 { t.Fatalf("expected to seek to start: %v != 0", start) } end, err := fr.Seek(0, os.SEEK_END) if err != nil { t.Fatalf("error checking current offset: %v", err) } if end != int64(len(content)) { t.Fatalf("expected to seek to end: %v != %v", end, len(content)) } // 4. Seek before start, ensure error. // seek before start before, err := fr.Seek(-1, os.SEEK_SET) if err == nil { t.Fatalf("error expected, returned offset=%v", before) } // 5. Seek after end, after, err := fr.Seek(1, os.SEEK_END) if err != nil { t.Fatalf("unexpected error expected, returned offset=%v", after) } p := make([]byte, 16) n, err := fr.Read(p) if n != 0 { t.Fatalf("bytes reads %d != %d", n, 0) } if err != io.EOF { t.Fatalf("expected io.EOF, got %v", err) } } // TestFileReaderNonExistentFile ensures the reader behaves as expected with a // missing or zero-length remote file. While the file may not exist, the // reader should not error out on creation and should return 0-bytes from the // read method, with an io.EOF error. func TestFileReaderNonExistentFile(t *testing.T) { driver := inmemory.New() fr, err := newFileReader(context.Background(), driver, "/doesnotexist", 10) if err != nil { t.Fatalf("unexpected error initializing reader: %v", err) } var buf [1024]byte n, err := fr.Read(buf[:]) if n != 0 { t.Fatalf("non-zero byte read reported: %d != 0", n) } if err != io.EOF { t.Fatalf("read on missing file should return io.EOF, got %v", err) } } // TestLayerReadErrors covers the various error return type for different // conditions that can arise when reading a layer. func TestFileReaderErrors(t *testing.T) { // TODO(stevvooe): We need to cover error return types, driven by the // errors returned via the HTTP API. For now, here is an incomplete list: // // 1. Layer Not Found: returned when layer is not found or access is // denied. // 2. Layer Unavailable: returned when link references are unresolved, // but layer is known to the registry. // 3. Layer Invalid: This may more split into more errors, but should be // returned when name or tarsum does not reference a valid error. We // may also need something to communication layer verification errors // for the inline tarsum check. // 4. Timeout: timeouts to backend. Need to better understand these // failure cases and how the storage driver propagates these errors // up the stack. } docker-registry-2.6.2~ds1/registry/storage/garbagecollect.go000066400000000000000000000062271313450123100242450ustar00rootroot00000000000000package storage import ( "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver" ) func emit(format string, a ...interface{}) { fmt.Printf(format+"\n", a...) } // MarkAndSweep performs a mark and sweep of registry data func MarkAndSweep(ctx context.Context, storageDriver driver.StorageDriver, registry distribution.Namespace, dryRun bool) error { repositoryEnumerator, ok := registry.(distribution.RepositoryEnumerator) if !ok { return fmt.Errorf("unable to convert Namespace to RepositoryEnumerator") } // mark markSet := make(map[digest.Digest]struct{}) err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { emit(repoName) var err error named, err := reference.ParseNamed(repoName) if err != nil { return fmt.Errorf("failed to parse repo name %s: %v", repoName, err) } repository, err := registry.Repository(ctx, named) if err != nil { return fmt.Errorf("failed to construct repository: %v", err) } manifestService, err := repository.Manifests(ctx) if err != nil { return fmt.Errorf("failed to construct manifest service: %v", err) } manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator) if !ok { return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator") } err = manifestEnumerator.Enumerate(ctx, func(dgst digest.Digest) error { // Mark the manifest's blob emit("%s: marking manifest %s ", repoName, dgst) markSet[dgst] = struct{}{} manifest, err := manifestService.Get(ctx, dgst) if err != nil { return fmt.Errorf("failed to retrieve manifest for digest %v: %v", dgst, err) } descriptors := manifest.References() for _, descriptor := range descriptors { markSet[descriptor.Digest] = struct{}{} emit("%s: marking blob %s", repoName, descriptor.Digest) } return nil }) if err != nil { // In certain situations such as unfinished uploads, deleting all // tags in S3 or removing the _manifests folder manually, this // error may be of type PathNotFound. // // In these cases we can continue marking other manifests safely. if _, ok := err.(driver.PathNotFoundError); ok { return nil } } return err }) if err != nil { return fmt.Errorf("failed to mark: %v", err) } // sweep blobService := registry.Blobs() deleteSet := make(map[digest.Digest]struct{}) err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { // check if digest is in markSet. If not, delete it! if _, ok := markSet[dgst]; !ok { deleteSet[dgst] = struct{}{} } return nil }) if err != nil { return fmt.Errorf("error enumerating blobs: %v", err) } emit("\n%d blobs marked, %d blobs eligible for deletion", len(markSet), len(deleteSet)) // Construct vacuum vacuum := NewVacuum(ctx, storageDriver) for dgst := range deleteSet { emit("blob eligible for deletion: %s", dgst) if dryRun { continue } err = vacuum.RemoveBlob(string(dgst)) if err != nil { return fmt.Errorf("failed to delete blob %s: %v", dgst, err) } } return err } docker-registry-2.6.2~ds1/registry/storage/garbagecollect_test.go000066400000000000000000000231141313450123100252760ustar00rootroot00000000000000package storage import ( "io" "path" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" "github.com/docker/libtrust" ) type image struct { manifest distribution.Manifest manifestDigest digest.Digest layers map[digest.Digest]io.ReadSeeker } func createRegistry(t *testing.T, driver driver.StorageDriver, options ...RegistryOption) distribution.Namespace { ctx := context.Background() k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } options = append([]RegistryOption{EnableDelete, Schema1SigningKey(k)}, options...) registry, err := NewRegistry(ctx, driver, options...) if err != nil { t.Fatalf("Failed to construct namespace") } return registry } func makeRepository(t *testing.T, registry distribution.Namespace, name string) distribution.Repository { ctx := context.Background() // Initialize a dummy repository named, err := reference.ParseNamed(name) if err != nil { t.Fatalf("Failed to parse name %s: %v", name, err) } repo, err := registry.Repository(ctx, named) if err != nil { t.Fatalf("Failed to construct repository: %v", err) } return repo } func makeManifestService(t *testing.T, repository distribution.Repository) distribution.ManifestService { ctx := context.Background() manifestService, err := repository.Manifests(ctx) if err != nil { t.Fatalf("Failed to construct manifest store: %v", err) } return manifestService } func allBlobs(t *testing.T, registry distribution.Namespace) map[digest.Digest]struct{} { ctx := context.Background() blobService := registry.Blobs() allBlobsMap := make(map[digest.Digest]struct{}) err := blobService.Enumerate(ctx, func(dgst digest.Digest) error { allBlobsMap[dgst] = struct{}{} return nil }) if err != nil { t.Fatalf("Error getting all blobs: %v", err) } return allBlobsMap } func uploadImage(t *testing.T, repository distribution.Repository, im image) digest.Digest { // upload layers err := testutil.UploadBlobs(repository, im.layers) if err != nil { t.Fatalf("layer upload failed: %v", err) } // upload manifest ctx := context.Background() manifestService := makeManifestService(t, repository) manifestDigest, err := manifestService.Put(ctx, im.manifest) if err != nil { t.Fatalf("manifest upload failed: %v", err) } return manifestDigest } func uploadRandomSchema1Image(t *testing.T, repository distribution.Repository) image { randomLayers, err := testutil.CreateRandomLayers(2) if err != nil { t.Fatalf("%v", err) } digests := []digest.Digest{} for digest := range randomLayers { digests = append(digests, digest) } manifest, err := testutil.MakeSchema1Manifest(digests) if err != nil { t.Fatalf("%v", err) } manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) return image{ manifest: manifest, manifestDigest: manifestDigest, layers: randomLayers, } } func uploadRandomSchema2Image(t *testing.T, repository distribution.Repository) image { randomLayers, err := testutil.CreateRandomLayers(2) if err != nil { t.Fatalf("%v", err) } digests := []digest.Digest{} for digest := range randomLayers { digests = append(digests, digest) } manifest, err := testutil.MakeSchema2Manifest(repository, digests) if err != nil { t.Fatalf("%v", err) } manifestDigest := uploadImage(t, repository, image{manifest: manifest, layers: randomLayers}) return image{ manifest: manifest, manifestDigest: manifestDigest, layers: randomLayers, } } func TestNoDeletionNoEffect(t *testing.T) { ctx := context.Background() inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver) repo := makeRepository(t, registry, "palailogos") manifestService, err := repo.Manifests(ctx) image1 := uploadRandomSchema1Image(t, repo) image2 := uploadRandomSchema1Image(t, repo) uploadRandomSchema2Image(t, repo) // construct manifestlist for fun. blobstatter := registry.BlobStatter() manifestList, err := testutil.MakeManifestList(blobstatter, []digest.Digest{ image1.manifestDigest, image2.manifestDigest}) if err != nil { t.Fatalf("Failed to make manifest list: %v", err) } _, err = manifestService.Put(ctx, manifestList) if err != nil { t.Fatalf("Failed to add manifest list: %v", err) } before := allBlobs(t, registry) // Run GC err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) if err != nil { t.Fatalf("Failed mark and sweep: %v", err) } after := allBlobs(t, registry) if len(before) != len(after) { t.Fatalf("Garbage collection affected storage: %d != %d", len(before), len(after)) } } func TestGCWithMissingManifests(t *testing.T) { ctx := context.Background() d := inmemory.New() registry := createRegistry(t, d) repo := makeRepository(t, registry, "testrepo") uploadRandomSchema1Image(t, repo) // Simulate a missing _manifests directory revPath, err := pathFor(manifestRevisionsPathSpec{"testrepo"}) if err != nil { t.Fatal(err) } _manifestsPath := path.Dir(revPath) err = d.Delete(ctx, _manifestsPath) if err != nil { t.Fatal(err) } err = MarkAndSweep(context.Background(), d, registry, false) if err != nil { t.Fatalf("Failed mark and sweep: %v", err) } blobs := allBlobs(t, registry) if len(blobs) > 0 { t.Errorf("unexpected blobs after gc") } } func TestDeletionHasEffect(t *testing.T) { ctx := context.Background() inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver) repo := makeRepository(t, registry, "komnenos") manifests, err := repo.Manifests(ctx) image1 := uploadRandomSchema1Image(t, repo) image2 := uploadRandomSchema1Image(t, repo) image3 := uploadRandomSchema2Image(t, repo) manifests.Delete(ctx, image2.manifestDigest) manifests.Delete(ctx, image3.manifestDigest) // Run GC err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) if err != nil { t.Fatalf("Failed mark and sweep: %v", err) } blobs := allBlobs(t, registry) // check that the image1 manifest and all the layers are still in blobs if _, ok := blobs[image1.manifestDigest]; !ok { t.Fatalf("First manifest is missing") } for layer := range image1.layers { if _, ok := blobs[layer]; !ok { t.Fatalf("manifest 1 layer is missing: %v", layer) } } // check that image2 and image3 layers are not still around for layer := range image2.layers { if _, ok := blobs[layer]; ok { t.Fatalf("manifest 2 layer is present: %v", layer) } } for layer := range image3.layers { if _, ok := blobs[layer]; ok { t.Fatalf("manifest 3 layer is present: %v", layer) } } } func getAnyKey(digests map[digest.Digest]io.ReadSeeker) (d digest.Digest) { for d = range digests { break } return } func getKeys(digests map[digest.Digest]io.ReadSeeker) (ds []digest.Digest) { for d := range digests { ds = append(ds, d) } return } func TestDeletionWithSharedLayer(t *testing.T) { ctx := context.Background() inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver) repo := makeRepository(t, registry, "tzimiskes") // Create random layers randomLayers1, err := testutil.CreateRandomLayers(3) if err != nil { t.Fatalf("failed to make layers: %v", err) } randomLayers2, err := testutil.CreateRandomLayers(3) if err != nil { t.Fatalf("failed to make layers: %v", err) } // Upload all layers err = testutil.UploadBlobs(repo, randomLayers1) if err != nil { t.Fatalf("failed to upload layers: %v", err) } err = testutil.UploadBlobs(repo, randomLayers2) if err != nil { t.Fatalf("failed to upload layers: %v", err) } // Construct manifests manifest1, err := testutil.MakeSchema1Manifest(getKeys(randomLayers1)) if err != nil { t.Fatalf("failed to make manifest: %v", err) } sharedKey := getAnyKey(randomLayers1) manifest2, err := testutil.MakeSchema2Manifest(repo, append(getKeys(randomLayers2), sharedKey)) if err != nil { t.Fatalf("failed to make manifest: %v", err) } manifestService := makeManifestService(t, repo) // Upload manifests _, err = manifestService.Put(ctx, manifest1) if err != nil { t.Fatalf("manifest upload failed: %v", err) } manifestDigest2, err := manifestService.Put(ctx, manifest2) if err != nil { t.Fatalf("manifest upload failed: %v", err) } // delete err = manifestService.Delete(ctx, manifestDigest2) if err != nil { t.Fatalf("manifest deletion failed: %v", err) } // check that all of the layers in layer 1 are still there blobs := allBlobs(t, registry) for dgst := range randomLayers1 { if _, ok := blobs[dgst]; !ok { t.Fatalf("random layer 1 blob missing: %v", dgst) } } } func TestOrphanBlobDeleted(t *testing.T) { inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver) repo := makeRepository(t, registry, "michael_z_doukas") digests, err := testutil.CreateRandomLayers(1) if err != nil { t.Fatalf("Failed to create random digest: %v", err) } if err = testutil.UploadBlobs(repo, digests); err != nil { t.Fatalf("Failed to upload blob: %v", err) } // formality to create the necessary directories uploadRandomSchema2Image(t, repo) // Run GC err = MarkAndSweep(context.Background(), inmemoryDriver, registry, false) if err != nil { t.Fatalf("Failed mark and sweep: %v", err) } blobs := allBlobs(t, registry) // check that orphan blob layers are not still around for dgst := range digests { if _, ok := blobs[dgst]; ok { t.Fatalf("Orphan layer is present: %v", dgst) } } } docker-registry-2.6.2~ds1/registry/storage/io.go000066400000000000000000000030411313450123100217050ustar00rootroot00000000000000package storage import ( "errors" "io" "io/ioutil" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage/driver" ) const ( maxBlobGetSize = 4 << 20 ) func getContent(ctx context.Context, driver driver.StorageDriver, p string) ([]byte, error) { r, err := driver.Reader(ctx, p, 0) if err != nil { return nil, err } return readAllLimited(r, maxBlobGetSize) } func readAllLimited(r io.Reader, limit int64) ([]byte, error) { r = limitReader(r, limit) return ioutil.ReadAll(r) } // limitReader returns a new reader limited to n bytes. Unlike io.LimitReader, // this returns an error when the limit reached. func limitReader(r io.Reader, n int64) io.Reader { return &limitedReader{r: r, n: n} } // limitedReader implements a reader that errors when the limit is reached. // // Partially cribbed from net/http.MaxBytesReader. type limitedReader struct { r io.Reader // underlying reader n int64 // max bytes remaining err error // sticky error } func (l *limitedReader) Read(p []byte) (n int, err error) { if l.err != nil { return 0, l.err } if len(p) == 0 { return 0, nil } // If they asked for a 32KB byte read but only 5 bytes are // remaining, no need to read 32KB. 6 bytes will answer the // question of the whether we hit the limit or go past it. if int64(len(p)) > l.n+1 { p = p[:l.n+1] } n, err = l.r.Read(p) if int64(n) <= l.n { l.n -= int64(n) l.err = err return n, err } n = int(l.n) l.n = 0 l.err = errors.New("storage: read exceeds limit") return n, l.err } docker-registry-2.6.2~ds1/registry/storage/linkedblobstore.go000066400000000000000000000322221313450123100244630ustar00rootroot00000000000000package storage import ( "fmt" "net/http" "path" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/uuid" ) // linkPathFunc describes a function that can resolve a link based on the // repository name and digest. type linkPathFunc func(name string, dgst digest.Digest) (string, error) // linkedBlobStore provides a full BlobService that namespaces the blobs to a // given repository. Effectively, it manages the links in a given repository // that grant access to the global blob store. type linkedBlobStore struct { *blobStore registry *registry blobServer distribution.BlobServer blobAccessController distribution.BlobDescriptorService repository distribution.Repository ctx context.Context // only to be used where context can't come through method args deleteEnabled bool resumableDigestEnabled bool // linkPathFns specifies one or more path functions allowing one to // control the repository blob link set to which the blob store // dispatches. This is required because manifest and layer blobs have not // yet been fully merged. At some point, this functionality should be // removed the blob links folder should be merged. The first entry is // treated as the "canonical" link location and will be used for writes. linkPathFns []linkPathFunc // linkDirectoryPathSpec locates the root directories in which one might find links linkDirectoryPathSpec pathSpec } var _ distribution.BlobStore = &linkedBlobStore{} func (lbs *linkedBlobStore) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { return lbs.blobAccessController.Stat(ctx, dgst) } func (lbs *linkedBlobStore) Get(ctx context.Context, dgst digest.Digest) ([]byte, error) { canonical, err := lbs.Stat(ctx, dgst) // access check if err != nil { return nil, err } return lbs.blobStore.Get(ctx, canonical.Digest) } func (lbs *linkedBlobStore) Open(ctx context.Context, dgst digest.Digest) (distribution.ReadSeekCloser, error) { canonical, err := lbs.Stat(ctx, dgst) // access check if err != nil { return nil, err } return lbs.blobStore.Open(ctx, canonical.Digest) } func (lbs *linkedBlobStore) ServeBlob(ctx context.Context, w http.ResponseWriter, r *http.Request, dgst digest.Digest) error { canonical, err := lbs.Stat(ctx, dgst) // access check if err != nil { return err } if canonical.MediaType != "" { // Set the repository local content type. w.Header().Set("Content-Type", canonical.MediaType) } return lbs.blobServer.ServeBlob(ctx, w, r, canonical.Digest) } func (lbs *linkedBlobStore) Put(ctx context.Context, mediaType string, p []byte) (distribution.Descriptor, error) { dgst := digest.FromBytes(p) // Place the data in the blob store first. desc, err := lbs.blobStore.Put(ctx, mediaType, p) if err != nil { context.GetLogger(ctx).Errorf("error putting into main store: %v", err) return distribution.Descriptor{}, err } if err := lbs.blobAccessController.SetDescriptor(ctx, dgst, desc); err != nil { return distribution.Descriptor{}, err } // TODO(stevvooe): Write out mediatype if incoming differs from what is // returned by Put above. Note that we should allow updates for a given // repository. return desc, lbs.linkBlob(ctx, desc) } type optionFunc func(interface{}) error func (f optionFunc) Apply(v interface{}) error { return f(v) } // WithMountFrom returns a BlobCreateOption which designates that the blob should be // mounted from the given canonical reference. func WithMountFrom(ref reference.Canonical) distribution.BlobCreateOption { return optionFunc(func(v interface{}) error { opts, ok := v.(*distribution.CreateOptions) if !ok { return fmt.Errorf("unexpected options type: %T", v) } opts.Mount.ShouldMount = true opts.Mount.From = ref return nil }) } // Writer begins a blob write session, returning a handle. func (lbs *linkedBlobStore) Create(ctx context.Context, options ...distribution.BlobCreateOption) (distribution.BlobWriter, error) { context.GetLogger(ctx).Debug("(*linkedBlobStore).Writer") var opts distribution.CreateOptions for _, option := range options { err := option.Apply(&opts) if err != nil { return nil, err } } if opts.Mount.ShouldMount { desc, err := lbs.mount(ctx, opts.Mount.From, opts.Mount.From.Digest(), opts.Mount.Stat) if err == nil { // Mount successful, no need to initiate an upload session return nil, distribution.ErrBlobMounted{From: opts.Mount.From, Descriptor: desc} } } uuid := uuid.Generate().String() startedAt := time.Now().UTC() path, err := pathFor(uploadDataPathSpec{ name: lbs.repository.Named().Name(), id: uuid, }) if err != nil { return nil, err } startedAtPath, err := pathFor(uploadStartedAtPathSpec{ name: lbs.repository.Named().Name(), id: uuid, }) if err != nil { return nil, err } // Write a startedat file for this upload if err := lbs.blobStore.driver.PutContent(ctx, startedAtPath, []byte(startedAt.Format(time.RFC3339))); err != nil { return nil, err } return lbs.newBlobUpload(ctx, uuid, path, startedAt, false) } func (lbs *linkedBlobStore) Resume(ctx context.Context, id string) (distribution.BlobWriter, error) { context.GetLogger(ctx).Debug("(*linkedBlobStore).Resume") startedAtPath, err := pathFor(uploadStartedAtPathSpec{ name: lbs.repository.Named().Name(), id: id, }) if err != nil { return nil, err } startedAtBytes, err := lbs.blobStore.driver.GetContent(ctx, startedAtPath) if err != nil { switch err := err.(type) { case driver.PathNotFoundError: return nil, distribution.ErrBlobUploadUnknown default: return nil, err } } startedAt, err := time.Parse(time.RFC3339, string(startedAtBytes)) if err != nil { return nil, err } path, err := pathFor(uploadDataPathSpec{ name: lbs.repository.Named().Name(), id: id, }) if err != nil { return nil, err } return lbs.newBlobUpload(ctx, id, path, startedAt, true) } func (lbs *linkedBlobStore) Delete(ctx context.Context, dgst digest.Digest) error { if !lbs.deleteEnabled { return distribution.ErrUnsupported } // Ensure the blob is available for deletion _, err := lbs.blobAccessController.Stat(ctx, dgst) if err != nil { return err } err = lbs.blobAccessController.Clear(ctx, dgst) if err != nil { return err } return nil } func (lbs *linkedBlobStore) Enumerate(ctx context.Context, ingestor func(digest.Digest) error) error { rootPath, err := pathFor(lbs.linkDirectoryPathSpec) if err != nil { return err } err = Walk(ctx, lbs.blobStore.driver, rootPath, func(fileInfo driver.FileInfo) error { // exit early if directory... if fileInfo.IsDir() { return nil } filePath := fileInfo.Path() // check if it's a link _, fileName := path.Split(filePath) if fileName != "link" { return nil } // read the digest found in link digest, err := lbs.blobStore.readlink(ctx, filePath) if err != nil { return err } // ensure this conforms to the linkPathFns _, err = lbs.Stat(ctx, digest) if err != nil { // we expect this error to occur so we move on if err == distribution.ErrBlobUnknown { return nil } return err } err = ingestor(digest) if err != nil { return err } return nil }) if err != nil { return err } return nil } func (lbs *linkedBlobStore) mount(ctx context.Context, sourceRepo reference.Named, dgst digest.Digest, sourceStat *distribution.Descriptor) (distribution.Descriptor, error) { var stat distribution.Descriptor if sourceStat == nil { // look up the blob info from the sourceRepo if not already provided repo, err := lbs.registry.Repository(ctx, sourceRepo) if err != nil { return distribution.Descriptor{}, err } stat, err = repo.Blobs(ctx).Stat(ctx, dgst) if err != nil { return distribution.Descriptor{}, err } } else { // use the provided blob info stat = *sourceStat } desc := distribution.Descriptor{ Size: stat.Size, // NOTE(stevvooe): The central blob store firewalls media types from // other users. The caller should look this up and override the value // for the specific repository. MediaType: "application/octet-stream", Digest: dgst, } return desc, lbs.linkBlob(ctx, desc) } // newBlobUpload allocates a new upload controller with the given state. func (lbs *linkedBlobStore) newBlobUpload(ctx context.Context, uuid, path string, startedAt time.Time, append bool) (distribution.BlobWriter, error) { fw, err := lbs.driver.Writer(ctx, path, append) if err != nil { return nil, err } bw := &blobWriter{ ctx: ctx, blobStore: lbs, id: uuid, startedAt: startedAt, digester: digest.Canonical.New(), fileWriter: fw, driver: lbs.driver, path: path, resumableDigestEnabled: lbs.resumableDigestEnabled, } return bw, nil } // linkBlob links a valid, written blob into the registry under the named // repository for the upload controller. func (lbs *linkedBlobStore) linkBlob(ctx context.Context, canonical distribution.Descriptor, aliases ...digest.Digest) error { dgsts := append([]digest.Digest{canonical.Digest}, aliases...) // TODO(stevvooe): Need to write out mediatype for only canonical hash // since we don't care about the aliases. They are generally unused except // for tarsum but those versions don't care about mediatype. // Don't make duplicate links. seenDigests := make(map[digest.Digest]struct{}, len(dgsts)) // only use the first link linkPathFn := lbs.linkPathFns[0] for _, dgst := range dgsts { if _, seen := seenDigests[dgst]; seen { continue } seenDigests[dgst] = struct{}{} blobLinkPath, err := linkPathFn(lbs.repository.Named().Name(), dgst) if err != nil { return err } if err := lbs.blobStore.link(ctx, blobLinkPath, canonical.Digest); err != nil { return err } } return nil } type linkedBlobStatter struct { *blobStore repository distribution.Repository // linkPathFns specifies one or more path functions allowing one to // control the repository blob link set to which the blob store // dispatches. This is required because manifest and layer blobs have not // yet been fully merged. At some point, this functionality should be // removed an the blob links folder should be merged. The first entry is // treated as the "canonical" link location and will be used for writes. linkPathFns []linkPathFunc } var _ distribution.BlobDescriptorService = &linkedBlobStatter{} func (lbs *linkedBlobStatter) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { var ( found bool target digest.Digest ) // try the many link path functions until we get success or an error that // is not PathNotFoundError. for _, linkPathFn := range lbs.linkPathFns { var err error target, err = lbs.resolveWithLinkFunc(ctx, dgst, linkPathFn) if err == nil { found = true break // success! } switch err := err.(type) { case driver.PathNotFoundError: // do nothing, just move to the next linkPathFn default: return distribution.Descriptor{}, err } } if !found { return distribution.Descriptor{}, distribution.ErrBlobUnknown } if target != dgst { // Track when we are doing cross-digest domain lookups. ie, sha512 to sha256. context.GetLogger(ctx).Warnf("looking up blob with canonical target: %v -> %v", dgst, target) } // TODO(stevvooe): Look up repository local mediatype and replace that on // the returned descriptor. return lbs.blobStore.statter.Stat(ctx, target) } func (lbs *linkedBlobStatter) Clear(ctx context.Context, dgst digest.Digest) (err error) { // clear any possible existence of a link described in linkPathFns for _, linkPathFn := range lbs.linkPathFns { blobLinkPath, err := linkPathFn(lbs.repository.Named().Name(), dgst) if err != nil { return err } err = lbs.blobStore.driver.Delete(ctx, blobLinkPath) if err != nil { switch err := err.(type) { case driver.PathNotFoundError: continue // just ignore this error and continue default: return err } } } return nil } // resolveTargetWithFunc allows us to read a link to a resource with different // linkPathFuncs to let us try a few different paths before returning not // found. func (lbs *linkedBlobStatter) resolveWithLinkFunc(ctx context.Context, dgst digest.Digest, linkPathFn linkPathFunc) (digest.Digest, error) { blobLinkPath, err := linkPathFn(lbs.repository.Named().Name(), dgst) if err != nil { return "", err } return lbs.blobStore.readlink(ctx, blobLinkPath) } func (lbs *linkedBlobStatter) SetDescriptor(ctx context.Context, dgst digest.Digest, desc distribution.Descriptor) error { // The canonical descriptor for a blob is set at the commit phase of upload return nil } // blobLinkPath provides the path to the blob link, also known as layers. func blobLinkPath(name string, dgst digest.Digest) (string, error) { return pathFor(layerLinkPathSpec{name: name, digest: dgst}) } // manifestRevisionLinkPath provides the path to the manifest revision link. func manifestRevisionLinkPath(name string, dgst digest.Digest) (string, error) { return pathFor(manifestRevisionLinkPathSpec{name: name, revision: dgst}) } docker-registry-2.6.2~ds1/registry/storage/linkedblobstore_test.go000066400000000000000000000146611313450123100255310ustar00rootroot00000000000000package storage import ( "fmt" "io" "reflect" "strconv" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/reference" "github.com/docker/distribution/testutil" ) func TestLinkedBlobStoreCreateWithMountFrom(t *testing.T) { fooRepoName, _ := reference.ParseNamed("nm/foo") fooEnv := newManifestStoreTestEnv(t, fooRepoName, "thetag") ctx := context.Background() stats, err := mockRegistry(t, fooEnv.registry) if err != nil { t.Fatal(err) } // Build up some test layers and add them to the manifest, saving the // readseekers for upload later. testLayers := map[digest.Digest]io.ReadSeeker{} for i := 0; i < 2; i++ { rs, ds, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } dgst := digest.Digest(ds) testLayers[digest.Digest(dgst)] = rs } // upload the layers to foo/bar for dgst, rs := range testLayers { wr, err := fooEnv.repository.Blobs(fooEnv.ctx).Create(fooEnv.ctx) if err != nil { t.Fatalf("unexpected error creating test upload: %v", err) } if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(fooEnv.ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } // create another repository nm/bar barRepoName, _ := reference.ParseNamed("nm/bar") barRepo, err := fooEnv.registry.Repository(ctx, barRepoName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } // cross-repo mount the test layers into a nm/bar for dgst := range testLayers { fooCanonical, _ := reference.WithDigest(fooRepoName, dgst) option := WithMountFrom(fooCanonical) // ensure we can instrospect it createOpts := distribution.CreateOptions{} if err := option.Apply(&createOpts); err != nil { t.Fatalf("failed to apply MountFrom option: %v", err) } if !createOpts.Mount.ShouldMount || createOpts.Mount.From.String() != fooCanonical.String() { t.Fatalf("unexpected create options: %#+v", createOpts.Mount) } _, err := barRepo.Blobs(ctx).Create(ctx, WithMountFrom(fooCanonical)) if err == nil { t.Fatalf("unexpected non-error while mounting from %q: %v", fooRepoName.String(), err) } if _, ok := err.(distribution.ErrBlobMounted); !ok { t.Fatalf("expected ErrMountFrom error, not %T: %v", err, err) } } for dgst := range testLayers { fooCanonical, _ := reference.WithDigest(fooRepoName, dgst) count, exists := stats[fooCanonical.String()] if !exists { t.Errorf("expected entry %q not found among handled stat calls", fooCanonical.String()) } else if count != 1 { t.Errorf("expected exactly one stat call for entry %q, not %d", fooCanonical.String(), count) } } clearStats(stats) // create yet another repository nm/baz bazRepoName, _ := reference.ParseNamed("nm/baz") bazRepo, err := fooEnv.registry.Repository(ctx, bazRepoName) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } // cross-repo mount them into a nm/baz and provide a prepopulated blob descriptor for dgst := range testLayers { fooCanonical, _ := reference.WithDigest(fooRepoName, dgst) size, err := strconv.ParseInt("0x"+dgst.Hex()[:8], 0, 64) if err != nil { t.Fatal(err) } prepolutatedDescriptor := distribution.Descriptor{ Digest: dgst, Size: size, MediaType: "application/octet-stream", } _, err = bazRepo.Blobs(ctx).Create(ctx, WithMountFrom(fooCanonical), &statCrossMountCreateOption{ desc: prepolutatedDescriptor, }) blobMounted, ok := err.(distribution.ErrBlobMounted) if !ok { t.Errorf("expected ErrMountFrom error, not %T: %v", err, err) continue } if !reflect.DeepEqual(blobMounted.Descriptor, prepolutatedDescriptor) { t.Errorf("unexpected descriptor: %#+v != %#+v", blobMounted.Descriptor, prepolutatedDescriptor) } } // this time no stat calls will be made if len(stats) != 0 { t.Errorf("unexpected number of stats made: %d != %d", len(stats), len(testLayers)) } } func clearStats(stats map[string]int) { for k := range stats { delete(stats, k) } } // mockRegistry sets a mock blob descriptor service factory that overrides // statter's Stat method to note each attempt to stat a blob in any repository. // Returned stats map contains canonical references to blobs with a number of // attempts. func mockRegistry(t *testing.T, nm distribution.Namespace) (map[string]int, error) { registry, ok := nm.(*registry) if !ok { return nil, fmt.Errorf("not an expected type of registry: %T", nm) } stats := make(map[string]int) registry.blobDescriptorServiceFactory = &mockBlobDescriptorServiceFactory{ t: t, stats: stats, } return stats, nil } type mockBlobDescriptorServiceFactory struct { t *testing.T stats map[string]int } func (f *mockBlobDescriptorServiceFactory) BlobAccessController(svc distribution.BlobDescriptorService) distribution.BlobDescriptorService { return &mockBlobDescriptorService{ BlobDescriptorService: svc, t: f.t, stats: f.stats, } } type mockBlobDescriptorService struct { distribution.BlobDescriptorService t *testing.T stats map[string]int } var _ distribution.BlobDescriptorService = &mockBlobDescriptorService{} func (bs *mockBlobDescriptorService) Stat(ctx context.Context, dgst digest.Digest) (distribution.Descriptor, error) { statter, ok := bs.BlobDescriptorService.(*linkedBlobStatter) if !ok { return distribution.Descriptor{}, fmt.Errorf("unexpected blob descriptor service: %T", bs.BlobDescriptorService) } name := statter.repository.Named() canonical, err := reference.WithDigest(name, dgst) if err != nil { return distribution.Descriptor{}, fmt.Errorf("failed to make canonical reference: %v", err) } bs.stats[canonical.String()]++ bs.t.Logf("calling Stat on %s", canonical.String()) return bs.BlobDescriptorService.Stat(ctx, dgst) } // statCrossMountCreateOptions ensures the expected options type is passed, and optionally pre-fills the cross-mount stat info type statCrossMountCreateOption struct { desc distribution.Descriptor } var _ distribution.BlobCreateOption = statCrossMountCreateOption{} func (f statCrossMountCreateOption) Apply(v interface{}) error { opts, ok := v.(*distribution.CreateOptions) if !ok { return fmt.Errorf("Unexpected create options: %#v", v) } if !opts.Mount.ShouldMount { return nil } opts.Mount.Stat = &f.desc return nil } docker-registry-2.6.2~ds1/registry/storage/manifestlisthandler.go000066400000000000000000000053271313450123100253470ustar00rootroot00000000000000package storage import ( "fmt" "encoding/json" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/manifestlist" ) // manifestListHandler is a ManifestHandler that covers schema2 manifest lists. type manifestListHandler struct { repository distribution.Repository blobStore distribution.BlobStore ctx context.Context } var _ ManifestHandler = &manifestListHandler{} func (ms *manifestListHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Unmarshal") var m manifestlist.DeserializedManifestList if err := json.Unmarshal(content, &m); err != nil { return nil, err } return &m, nil } func (ms *manifestListHandler) Put(ctx context.Context, manifestList distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { context.GetLogger(ms.ctx).Debug("(*manifestListHandler).Put") m, ok := manifestList.(*manifestlist.DeserializedManifestList) if !ok { return "", fmt.Errorf("wrong type put to manifestListHandler: %T", manifestList) } if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil { return "", err } mt, payload, err := m.Payload() if err != nil { return "", err } revision, err := ms.blobStore.Put(ctx, mt, payload) if err != nil { context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) return "", err } return revision.Digest, nil } // verifyManifest ensures that the manifest content is valid from the // perspective of the registry. As a policy, the registry only tries to // store valid content, leaving trust policies of that content up to // consumers. func (ms *manifestListHandler) verifyManifest(ctx context.Context, mnfst manifestlist.DeserializedManifestList, skipDependencyVerification bool) error { var errs distribution.ErrManifestVerification if !skipDependencyVerification { // This manifest service is different from the blob service // returned by Blob. It uses a linked blob store to ensure that // only manifests are accessible. manifestService, err := ms.repository.Manifests(ctx) if err != nil { return err } for _, manifestDescriptor := range mnfst.References() { exists, err := manifestService.Exists(ctx, manifestDescriptor.Digest) if err != nil && err != distribution.ErrBlobUnknown { errs = append(errs, err) } if err != nil || !exists { // On error here, we always append unknown blob errors. errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: manifestDescriptor.Digest}) } } } if len(errs) != 0 { return errs } return nil } docker-registry-2.6.2~ds1/registry/storage/manifeststore.go000066400000000000000000000105141313450123100241640ustar00rootroot00000000000000package storage import ( "fmt" "encoding/json" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" ) // A ManifestHandler gets and puts manifests of a particular type. type ManifestHandler interface { // Unmarshal unmarshals the manifest from a byte slice. Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) // Put creates or updates the given manifest returning the manifest digest. Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) } // SkipLayerVerification allows a manifest to be Put before its // layers are on the filesystem func SkipLayerVerification() distribution.ManifestServiceOption { return skipLayerOption{} } type skipLayerOption struct{} func (o skipLayerOption) Apply(m distribution.ManifestService) error { if ms, ok := m.(*manifestStore); ok { ms.skipDependencyVerification = true return nil } return fmt.Errorf("skip layer verification only valid for manifestStore") } type manifestStore struct { repository *repository blobStore *linkedBlobStore ctx context.Context skipDependencyVerification bool schema1Handler ManifestHandler schema2Handler ManifestHandler manifestListHandler ManifestHandler } var _ distribution.ManifestService = &manifestStore{} func (ms *manifestStore) Exists(ctx context.Context, dgst digest.Digest) (bool, error) { context.GetLogger(ms.ctx).Debug("(*manifestStore).Exists") _, err := ms.blobStore.Stat(ms.ctx, dgst) if err != nil { if err == distribution.ErrBlobUnknown { return false, nil } return false, err } return true, nil } func (ms *manifestStore) Get(ctx context.Context, dgst digest.Digest, options ...distribution.ManifestServiceOption) (distribution.Manifest, error) { context.GetLogger(ms.ctx).Debug("(*manifestStore).Get") // TODO(stevvooe): Need to check descriptor from above to ensure that the // mediatype is as we expect for the manifest store. content, err := ms.blobStore.Get(ctx, dgst) if err != nil { if err == distribution.ErrBlobUnknown { return nil, distribution.ErrManifestUnknownRevision{ Name: ms.repository.Named().Name(), Revision: dgst, } } return nil, err } var versioned manifest.Versioned if err = json.Unmarshal(content, &versioned); err != nil { return nil, err } switch versioned.SchemaVersion { case 1: return ms.schema1Handler.Unmarshal(ctx, dgst, content) case 2: // This can be an image manifest or a manifest list switch versioned.MediaType { case schema2.MediaTypeManifest: return ms.schema2Handler.Unmarshal(ctx, dgst, content) case manifestlist.MediaTypeManifestList: return ms.manifestListHandler.Unmarshal(ctx, dgst, content) default: return nil, distribution.ErrManifestVerification{fmt.Errorf("unrecognized manifest content type %s", versioned.MediaType)} } } return nil, fmt.Errorf("unrecognized manifest schema version %d", versioned.SchemaVersion) } func (ms *manifestStore) Put(ctx context.Context, manifest distribution.Manifest, options ...distribution.ManifestServiceOption) (digest.Digest, error) { context.GetLogger(ms.ctx).Debug("(*manifestStore).Put") switch manifest.(type) { case *schema1.SignedManifest: return ms.schema1Handler.Put(ctx, manifest, ms.skipDependencyVerification) case *schema2.DeserializedManifest: return ms.schema2Handler.Put(ctx, manifest, ms.skipDependencyVerification) case *manifestlist.DeserializedManifestList: return ms.manifestListHandler.Put(ctx, manifest, ms.skipDependencyVerification) } return "", fmt.Errorf("unrecognized manifest type %T", manifest) } // Delete removes the revision of the specified manifest. func (ms *manifestStore) Delete(ctx context.Context, dgst digest.Digest) error { context.GetLogger(ms.ctx).Debug("(*manifestStore).Delete") return ms.blobStore.Delete(ctx, dgst) } func (ms *manifestStore) Enumerate(ctx context.Context, ingester func(digest.Digest) error) error { err := ms.blobStore.Enumerate(ctx, func(dgst digest.Digest) error { err := ingester(dgst) if err != nil { return err } return nil }) return err } docker-registry-2.6.2~ds1/registry/storage/manifeststore_test.go000066400000000000000000000245111313450123100252250ustar00rootroot00000000000000package storage import ( "bytes" "io" "reflect" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache/memory" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/testutil" "github.com/docker/libtrust" ) type manifestStoreTestEnv struct { ctx context.Context driver driver.StorageDriver registry distribution.Namespace repository distribution.Repository name reference.Named tag string } func newManifestStoreTestEnv(t *testing.T, name reference.Named, tag string, options ...RegistryOption) *manifestStoreTestEnv { ctx := context.Background() driver := inmemory.New() registry, err := NewRegistry(ctx, driver, options...) if err != nil { t.Fatalf("error creating registry: %v", err) } repo, err := registry.Repository(ctx, name) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } return &manifestStoreTestEnv{ ctx: ctx, driver: driver, registry: registry, repository: repo, name: name, tag: tag, } } func TestManifestStorage(t *testing.T) { k, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatal(err) } testManifestStorage(t, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableDelete, EnableRedirect, Schema1SigningKey(k)) } func testManifestStorage(t *testing.T, options ...RegistryOption) { repoName, _ := reference.ParseNamed("foo/bar") env := newManifestStoreTestEnv(t, repoName, "thetag", options...) ctx := context.Background() ms, err := env.repository.Manifests(ctx) if err != nil { t.Fatal(err) } m := schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: env.name.Name(), Tag: env.tag, } // Build up some test layers and add them to the manifest, saving the // readseekers for upload later. testLayers := map[digest.Digest]io.ReadSeeker{} for i := 0; i < 2; i++ { rs, ds, err := testutil.CreateRandomTarFile() if err != nil { t.Fatalf("unexpected error generating test layer file") } dgst := digest.Digest(ds) testLayers[digest.Digest(dgst)] = rs m.FSLayers = append(m.FSLayers, schema1.FSLayer{ BlobSum: dgst, }) m.History = append(m.History, schema1.History{ V1Compatibility: "", }) } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm, merr := schema1.Sign(&m, pk) if merr != nil { t.Fatalf("error signing manifest: %v", err) } _, err = ms.Put(ctx, sm) if err == nil { t.Fatalf("expected errors putting manifest with full verification") } switch err := err.(type) { case distribution.ErrManifestVerification: if len(err) != 2 { t.Fatalf("expected 2 verification errors: %#v", err) } for _, err := range err { if _, ok := err.(distribution.ErrManifestBlobUnknown); !ok { t.Fatalf("unexpected error type: %v", err) } } default: t.Fatalf("unexpected error verifying manifest: %v", err) } // Now, upload the layers that were missing! for dgst, rs := range testLayers { wr, err := env.repository.Blobs(env.ctx).Create(env.ctx) if err != nil { t.Fatalf("unexpected error creating test upload: %v", err) } if _, err := io.Copy(wr, rs); err != nil { t.Fatalf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(env.ctx, distribution.Descriptor{Digest: dgst}); err != nil { t.Fatalf("unexpected error finishing upload: %v", err) } } var manifestDigest digest.Digest if manifestDigest, err = ms.Put(ctx, sm); err != nil { t.Fatalf("unexpected error putting manifest: %v", err) } exists, err := ms.Exists(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error checking manifest existence: %#v", err) } if !exists { t.Fatalf("manifest should exist") } fromStore, err := ms.Get(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } fetchedManifest, ok := fromStore.(*schema1.SignedManifest) if !ok { t.Fatalf("unexpected manifest type from signedstore") } if !bytes.Equal(fetchedManifest.Canonical, sm.Canonical) { t.Fatalf("fetched payload does not match original payload: %q != %q", fetchedManifest.Canonical, sm.Canonical) } _, pl, err := fetchedManifest.Payload() if err != nil { t.Fatalf("error getting payload %#v", err) } fetchedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("unexpected error parsing jws: %v", err) } payload, err := fetchedJWS.Payload() if err != nil { t.Fatalf("unexpected error extracting payload: %v", err) } // Now that we have a payload, take a moment to check that the manifest is // return by the payload digest. dgst := digest.FromBytes(payload) exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("error checking manifest existence by digest: %v", err) } if !exists { t.Fatalf("manifest %s should exist", dgst) } fetchedByDigest, err := ms.Get(ctx, dgst) if err != nil { t.Fatalf("unexpected error fetching manifest by digest: %v", err) } byDigestManifest, ok := fetchedByDigest.(*schema1.SignedManifest) if !ok { t.Fatalf("unexpected manifest type from signedstore") } if !bytes.Equal(byDigestManifest.Canonical, fetchedManifest.Canonical) { t.Fatalf("fetched manifest not equal: %q != %q", byDigestManifest.Canonical, fetchedManifest.Canonical) } sigs, err := fetchedJWS.Signatures() if err != nil { t.Fatalf("unable to extract signatures: %v", err) } if len(sigs) != 1 { t.Fatalf("unexpected number of signatures: %d != %d", len(sigs), 1) } // Now, push the same manifest with a different key pk2, err := libtrust.GenerateECP256PrivateKey() if err != nil { t.Fatalf("unexpected error generating private key: %v", err) } sm2, err := schema1.Sign(&m, pk2) if err != nil { t.Fatalf("unexpected error signing manifest: %v", err) } _, pl, err = sm2.Payload() if err != nil { t.Fatalf("error getting payload %#v", err) } jws2, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("error parsing signature: %v", err) } sigs2, err := jws2.Signatures() if err != nil { t.Fatalf("unable to extract signatures: %v", err) } if len(sigs2) != 1 { t.Fatalf("unexpected number of signatures: %d != %d", len(sigs2), 1) } if manifestDigest, err = ms.Put(ctx, sm2); err != nil { t.Fatalf("unexpected error putting manifest: %v", err) } fromStore, err = ms.Get(ctx, manifestDigest) if err != nil { t.Fatalf("unexpected error fetching manifest: %v", err) } fetched, ok := fromStore.(*schema1.SignedManifest) if !ok { t.Fatalf("unexpected type from signed manifeststore : %T", fetched) } if _, err := schema1.Verify(fetched); err != nil { t.Fatalf("unexpected error verifying manifest: %v", err) } _, pl, err = fetched.Payload() if err != nil { t.Fatalf("error getting payload %#v", err) } receivedJWS, err := libtrust.ParsePrettySignature(pl, "signatures") if err != nil { t.Fatalf("unexpected error parsing jws: %v", err) } receivedPayload, err := receivedJWS.Payload() if err != nil { t.Fatalf("unexpected error extracting received payload: %v", err) } if !bytes.Equal(receivedPayload, payload) { t.Fatalf("payloads are not equal") } // Test deleting manifests err = ms.Delete(ctx, dgst) if err != nil { t.Fatalf("unexpected an error deleting manifest by digest: %v", err) } exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("Error querying manifest existence") } if exists { t.Errorf("Deleted manifest should not exist") } deletedManifest, err := ms.Get(ctx, dgst) if err == nil { t.Errorf("Unexpected success getting deleted manifest") } switch err.(type) { case distribution.ErrManifestUnknownRevision: break default: t.Errorf("Unexpected error getting deleted manifest: %s", reflect.ValueOf(err).Type()) } if deletedManifest != nil { t.Errorf("Deleted manifest get returned non-nil") } // Re-upload should restore manifest to a good state _, err = ms.Put(ctx, sm) if err != nil { t.Errorf("Error re-uploading deleted manifest") } exists, err = ms.Exists(ctx, dgst) if err != nil { t.Fatalf("Error querying manifest existence") } if !exists { t.Errorf("Restored manifest should exist") } deletedManifest, err = ms.Get(ctx, dgst) if err != nil { t.Errorf("Unexpected error getting manifest") } if deletedManifest == nil { t.Errorf("Deleted manifest get returned non-nil") } r, err := NewRegistry(ctx, env.driver, BlobDescriptorCacheProvider(memory.NewInMemoryBlobDescriptorCacheProvider()), EnableRedirect) if err != nil { t.Fatalf("error creating registry: %v", err) } repo, err := r.Repository(ctx, env.name) if err != nil { t.Fatalf("unexpected error getting repo: %v", err) } ms, err = repo.Manifests(ctx) if err != nil { t.Fatal(err) } err = ms.Delete(ctx, dgst) if err == nil { t.Errorf("Unexpected success deleting while disabled") } } // TestLinkPathFuncs ensures that the link path functions behavior are locked // down and implemented as expected. func TestLinkPathFuncs(t *testing.T) { for _, testcase := range []struct { repo string digest digest.Digest linkPathFn linkPathFunc expected string }{ { repo: "foo/bar", digest: "sha256:deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", linkPathFn: blobLinkPath, expected: "/docker/registry/v2/repositories/foo/bar/_layers/sha256/deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/link", }, { repo: "foo/bar", digest: "sha256:deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", linkPathFn: manifestRevisionLinkPath, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/revisions/sha256/deadbeaf98fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/link", }, } { p, err := testcase.linkPathFn(testcase.repo, testcase.digest) if err != nil { t.Fatalf("unexpected error calling linkPathFn(pm, %q, %q): %v", testcase.repo, testcase.digest, err) } if p != testcase.expected { t.Fatalf("incorrect path returned: %q != %q", p, testcase.expected) } } } docker-registry-2.6.2~ds1/registry/storage/paths.go000066400000000000000000000370361313450123100224300ustar00rootroot00000000000000package storage import ( "fmt" "path" "strings" "github.com/docker/distribution/digest" ) const ( storagePathVersion = "v2" // fixed storage layout version storagePathRoot = "/docker/registry/" // all driver paths have a prefix // TODO(stevvooe): Get rid of the "storagePathRoot". Initially, we though // storage path root would configurable for all drivers through this // package. In reality, we've found it simpler to do this on a per driver // basis. ) // pathFor maps paths based on "object names" and their ids. The "object // names" mapped by are internal to the storage system. // // The path layout in the storage backend is roughly as follows: // // /v2 // -> repositories/ // ->/ // -> _manifests/ // revisions // -> // -> link // tags/ // -> current/link // -> index // -> //link // -> _layers/ // // -> _uploads/ // data // startedat // hashstates// // -> blob/ // // // The storage backend layout is broken up into a content-addressable blob // store and repositories. The content-addressable blob store holds most data // throughout the backend, keyed by algorithm and digests of the underlying // content. Access to the blob store is controlled through links from the // repository to blobstore. // // A repository is made up of layers, manifests and tags. The layers component // is just a directory of layers which are "linked" into a repository. A layer // can only be accessed through a qualified repository name if it is linked in // the repository. Uploads of layers are managed in the uploads directory, // which is key by upload id. When all data for an upload is received, the // data is moved into the blob store and the upload directory is deleted. // Abandoned uploads can be garbage collected by reading the startedat file // and removing uploads that have been active for longer than a certain time. // // The third component of the repository directory is the manifests store, // which is made up of a revision store and tag store. Manifests are stored in // the blob store and linked into the revision store. // While the registry can save all revisions of a manifest, no relationship is // implied as to the ordering of changes to a manifest. The tag store provides // support for name, tag lookups of manifests, using "current/link" under a // named tag directory. An index is maintained to support deletions of all // revisions of a given manifest tag. // // We cover the path formats implemented by this path mapper below. // // Manifests: // // manifestRevisionsPathSpec: /v2/repositories//_manifests/revisions/ // manifestRevisionPathSpec: /v2/repositories//_manifests/revisions/// // manifestRevisionLinkPathSpec: /v2/repositories//_manifests/revisions///link // // Tags: // // manifestTagsPathSpec: /v2/repositories//_manifests/tags/ // manifestTagPathSpec: /v2/repositories//_manifests/tags// // manifestTagCurrentPathSpec: /v2/repositories//_manifests/tags//current/link // manifestTagIndexPathSpec: /v2/repositories//_manifests/tags//index/ // manifestTagIndexEntryPathSpec: /v2/repositories//_manifests/tags//index/// // manifestTagIndexEntryLinkPathSpec: /v2/repositories//_manifests/tags//index///link // // Blobs: // // layerLinkPathSpec: /v2/repositories//_layers///link // // Uploads: // // uploadDataPathSpec: /v2/repositories//_uploads//data // uploadStartedAtPathSpec: /v2/repositories//_uploads//startedat // uploadHashStatePathSpec: /v2/repositories//_uploads//hashstates// // // Blob Store: // // blobsPathSpec: /v2/blobs/ // blobPathSpec: /v2/blobs/// // blobDataPathSpec: /v2/blobs////data // blobMediaTypePathSpec: /v2/blobs////data // // For more information on the semantic meaning of each path and their // contents, please see the path spec documentation. func pathFor(spec pathSpec) (string, error) { // Switch on the path object type and return the appropriate path. At // first glance, one may wonder why we don't use an interface to // accomplish this. By keep the formatting separate from the pathSpec, we // keep separate the path generation componentized. These specs could be // passed to a completely different mapper implementation and generate a // different set of paths. // // For example, imagine migrating from one backend to the other: one could // build a filesystem walker that converts a string path in one version, // to an intermediate path object, than can be consumed and mapped by the // other version. rootPrefix := []string{storagePathRoot, storagePathVersion} repoPrefix := append(rootPrefix, "repositories") switch v := spec.(type) { case manifestRevisionsPathSpec: return path.Join(append(repoPrefix, v.name, "_manifests", "revisions")...), nil case manifestRevisionPathSpec: components, err := digestPathComponents(v.revision, false) if err != nil { return "", err } return path.Join(append(append(repoPrefix, v.name, "_manifests", "revisions"), components...)...), nil case manifestRevisionLinkPathSpec: root, err := pathFor(manifestRevisionPathSpec{ name: v.name, revision: v.revision, }) if err != nil { return "", err } return path.Join(root, "link"), nil case manifestTagsPathSpec: return path.Join(append(repoPrefix, v.name, "_manifests", "tags")...), nil case manifestTagPathSpec: root, err := pathFor(manifestTagsPathSpec{ name: v.name, }) if err != nil { return "", err } return path.Join(root, v.tag), nil case manifestTagCurrentPathSpec: root, err := pathFor(manifestTagPathSpec{ name: v.name, tag: v.tag, }) if err != nil { return "", err } return path.Join(root, "current", "link"), nil case manifestTagIndexPathSpec: root, err := pathFor(manifestTagPathSpec{ name: v.name, tag: v.tag, }) if err != nil { return "", err } return path.Join(root, "index"), nil case manifestTagIndexEntryLinkPathSpec: root, err := pathFor(manifestTagIndexEntryPathSpec{ name: v.name, tag: v.tag, revision: v.revision, }) if err != nil { return "", err } return path.Join(root, "link"), nil case manifestTagIndexEntryPathSpec: root, err := pathFor(manifestTagIndexPathSpec{ name: v.name, tag: v.tag, }) if err != nil { return "", err } components, err := digestPathComponents(v.revision, false) if err != nil { return "", err } return path.Join(root, path.Join(components...)), nil case layerLinkPathSpec: components, err := digestPathComponents(v.digest, false) if err != nil { return "", err } // TODO(stevvooe): Right now, all blobs are linked under "_layers". If // we have future migrations, we may want to rename this to "_blobs". // A migration strategy would simply leave existing items in place and // write the new paths, commit a file then delete the old files. blobLinkPathComponents := append(repoPrefix, v.name, "_layers") return path.Join(path.Join(append(blobLinkPathComponents, components...)...), "link"), nil case blobsPathSpec: blobsPathPrefix := append(rootPrefix, "blobs") return path.Join(blobsPathPrefix...), nil case blobPathSpec: components, err := digestPathComponents(v.digest, true) if err != nil { return "", err } blobPathPrefix := append(rootPrefix, "blobs") return path.Join(append(blobPathPrefix, components...)...), nil case blobDataPathSpec: components, err := digestPathComponents(v.digest, true) if err != nil { return "", err } components = append(components, "data") blobPathPrefix := append(rootPrefix, "blobs") return path.Join(append(blobPathPrefix, components...)...), nil case uploadDataPathSpec: return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "data")...), nil case uploadStartedAtPathSpec: return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "startedat")...), nil case uploadHashStatePathSpec: offset := fmt.Sprintf("%d", v.offset) if v.list { offset = "" // Limit to the prefix for listing offsets. } return path.Join(append(repoPrefix, v.name, "_uploads", v.id, "hashstates", string(v.alg), offset)...), nil case repositoriesRootPathSpec: return path.Join(repoPrefix...), nil default: // TODO(sday): This is an internal error. Ensure it doesn't escape (panic?). return "", fmt.Errorf("unknown path spec: %#v", v) } } // pathSpec is a type to mark structs as path specs. There is no // implementation because we'd like to keep the specs and the mappers // decoupled. type pathSpec interface { pathSpec() } // manifestRevisionsPathSpec describes the directory path for // a manifest revision. type manifestRevisionsPathSpec struct { name string } func (manifestRevisionsPathSpec) pathSpec() {} // manifestRevisionPathSpec describes the components of the directory path for // a manifest revision. type manifestRevisionPathSpec struct { name string revision digest.Digest } func (manifestRevisionPathSpec) pathSpec() {} // manifestRevisionLinkPathSpec describes the path components required to look // up the data link for a revision of a manifest. If this file is not present, // the manifest blob is not available in the given repo. The contents of this // file should just be the digest. type manifestRevisionLinkPathSpec struct { name string revision digest.Digest } func (manifestRevisionLinkPathSpec) pathSpec() {} // manifestTagsPathSpec describes the path elements required to point to the // manifest tags directory. type manifestTagsPathSpec struct { name string } func (manifestTagsPathSpec) pathSpec() {} // manifestTagPathSpec describes the path elements required to point to the // manifest tag links files under a repository. These contain a blob id that // can be used to look up the data and signatures. type manifestTagPathSpec struct { name string tag string } func (manifestTagPathSpec) pathSpec() {} // manifestTagCurrentPathSpec describes the link to the current revision for a // given tag. type manifestTagCurrentPathSpec struct { name string tag string } func (manifestTagCurrentPathSpec) pathSpec() {} // manifestTagCurrentPathSpec describes the link to the index of revisions // with the given tag. type manifestTagIndexPathSpec struct { name string tag string } func (manifestTagIndexPathSpec) pathSpec() {} // manifestTagIndexEntryPathSpec contains the entries of the index by revision. type manifestTagIndexEntryPathSpec struct { name string tag string revision digest.Digest } func (manifestTagIndexEntryPathSpec) pathSpec() {} // manifestTagIndexEntryLinkPathSpec describes the link to a revisions of a // manifest with given tag within the index. type manifestTagIndexEntryLinkPathSpec struct { name string tag string revision digest.Digest } func (manifestTagIndexEntryLinkPathSpec) pathSpec() {} // blobLinkPathSpec specifies a path for a blob link, which is a file with a // blob id. The blob link will contain a content addressable blob id reference // into the blob store. The format of the contents is as follows: // // : // // The following example of the file contents is more illustrative: // // sha256:96443a84ce518ac22acb2e985eda402b58ac19ce6f91980bde63726a79d80b36 // // This indicates that there is a blob with the id/digest, calculated via // sha256 that can be fetched from the blob store. type layerLinkPathSpec struct { name string digest digest.Digest } func (layerLinkPathSpec) pathSpec() {} // blobAlgorithmReplacer does some very simple path sanitization for user // input. Paths should be "safe" before getting this far due to strict digest // requirements but we can add further path conversion here, if needed. var blobAlgorithmReplacer = strings.NewReplacer( "+", "/", ".", "/", ";", "/", ) // blobsPathSpec contains the path for the blobs directory type blobsPathSpec struct{} func (blobsPathSpec) pathSpec() {} // blobPathSpec contains the path for the registry global blob store. type blobPathSpec struct { digest digest.Digest } func (blobPathSpec) pathSpec() {} // blobDataPathSpec contains the path for the registry global blob store. For // now, this contains layer data, exclusively. type blobDataPathSpec struct { digest digest.Digest } func (blobDataPathSpec) pathSpec() {} // uploadDataPathSpec defines the path parameters of the data file for // uploads. type uploadDataPathSpec struct { name string id string } func (uploadDataPathSpec) pathSpec() {} // uploadDataPathSpec defines the path parameters for the file that stores the // start time of an uploads. If it is missing, the upload is considered // unknown. Admittedly, the presence of this file is an ugly hack to make sure // we have a way to cleanup old or stalled uploads that doesn't rely on driver // FileInfo behavior. If we come up with a more clever way to do this, we // should remove this file immediately and rely on the startetAt field from // the client to enforce time out policies. type uploadStartedAtPathSpec struct { name string id string } func (uploadStartedAtPathSpec) pathSpec() {} // uploadHashStatePathSpec defines the path parameters for the file that stores // the hash function state of an upload at a specific byte offset. If `list` is // set, then the path mapper will generate a list prefix for all hash state // offsets for the upload identified by the name, id, and alg. type uploadHashStatePathSpec struct { name string id string alg digest.Algorithm offset int64 list bool } func (uploadHashStatePathSpec) pathSpec() {} // repositoriesRootPathSpec returns the root of repositories type repositoriesRootPathSpec struct { } func (repositoriesRootPathSpec) pathSpec() {} // digestPathComponents provides a consistent path breakdown for a given // digest. For a generic digest, it will be as follows: // // / // // If multilevel is true, the first two bytes of the digest will separate // groups of digest folder. It will be as follows: // // // // func digestPathComponents(dgst digest.Digest, multilevel bool) ([]string, error) { if err := dgst.Validate(); err != nil { return nil, err } algorithm := blobAlgorithmReplacer.Replace(string(dgst.Algorithm())) hex := dgst.Hex() prefix := []string{algorithm} var suffix []string if multilevel { suffix = append(suffix, hex[:2]) } suffix = append(suffix, hex) return append(prefix, suffix...), nil } // Reconstructs a digest from a path func digestFromPath(digestPath string) (digest.Digest, error) { digestPath = strings.TrimSuffix(digestPath, "/data") dir, hex := path.Split(digestPath) dir = path.Dir(dir) dir, next := path.Split(dir) // next is either the algorithm OR the first two characters in the hex string var algo string if next == hex[:2] { algo = path.Base(dir) } else { algo = next } dgst := digest.NewDigestFromHex(algo, hex) return dgst, dgst.Validate() } docker-registry-2.6.2~ds1/registry/storage/paths_test.go000066400000000000000000000075211313450123100234630ustar00rootroot00000000000000package storage import ( "testing" "github.com/docker/distribution/digest" ) func TestPathMapper(t *testing.T) { for _, testcase := range []struct { spec pathSpec expected string err error }{ { spec: manifestRevisionPathSpec{ name: "foo/bar", revision: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/revisions/sha256/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, { spec: manifestRevisionLinkPathSpec{ name: "foo/bar", revision: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/revisions/sha256/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789/link", }, { spec: manifestTagsPathSpec{ name: "foo/bar", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags", }, { spec: manifestTagPathSpec{ name: "foo/bar", tag: "thetag", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags/thetag", }, { spec: manifestTagCurrentPathSpec{ name: "foo/bar", tag: "thetag", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags/thetag/current/link", }, { spec: manifestTagIndexPathSpec{ name: "foo/bar", tag: "thetag", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags/thetag/index", }, { spec: manifestTagIndexEntryPathSpec{ name: "foo/bar", tag: "thetag", revision: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags/thetag/index/sha256/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, { spec: manifestTagIndexEntryLinkPathSpec{ name: "foo/bar", tag: "thetag", revision: "sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", }, expected: "/docker/registry/v2/repositories/foo/bar/_manifests/tags/thetag/index/sha256/abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789/link", }, { spec: uploadDataPathSpec{ name: "foo/bar", id: "asdf-asdf-asdf-adsf", }, expected: "/docker/registry/v2/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/data", }, { spec: uploadStartedAtPathSpec{ name: "foo/bar", id: "asdf-asdf-asdf-adsf", }, expected: "/docker/registry/v2/repositories/foo/bar/_uploads/asdf-asdf-asdf-adsf/startedat", }, } { p, err := pathFor(testcase.spec) if err != nil { t.Fatalf("unexpected generating path (%T): %v", testcase.spec, err) } if p != testcase.expected { t.Fatalf("unexpected path generated (%T): %q != %q", testcase.spec, p, testcase.expected) } } // Add a few test cases to ensure we cover some errors // Specify a path that requires a revision and get a digest validation error. badpath, err := pathFor(manifestRevisionPathSpec{ name: "foo/bar", }) if err == nil { t.Fatalf("expected an error when mapping an invalid revision: %s", badpath) } } func TestDigestFromPath(t *testing.T) { for _, testcase := range []struct { path string expected digest.Digest multilevel bool err error }{ { path: "/docker/registry/v2/blobs/sha256/99/9943fffae777400c0344c58869c4c2619c329ca3ad4df540feda74d291dd7c86/data", multilevel: true, expected: "sha256:9943fffae777400c0344c58869c4c2619c329ca3ad4df540feda74d291dd7c86", err: nil, }, } { result, err := digestFromPath(testcase.path) if err != testcase.err { t.Fatalf("Unexpected error value %v when we wanted %v", err, testcase.err) } if result != testcase.expected { t.Fatalf("Unexpected result value %v when we wanted %v", result, testcase.expected) } } } docker-registry-2.6.2~ds1/registry/storage/purgeuploads.go000066400000000000000000000077201313450123100240200ustar00rootroot00000000000000package storage import ( "path" "strings" "time" log "github.com/Sirupsen/logrus" "github.com/docker/distribution/context" storageDriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/uuid" ) // uploadData stored the location of temporary files created during a layer upload // along with the date the upload was started type uploadData struct { containingDir string startedAt time.Time } func newUploadData() uploadData { return uploadData{ containingDir: "", // default to far in future to protect against missing startedat startedAt: time.Now().Add(time.Duration(10000 * time.Hour)), } } // PurgeUploads deletes files from the upload directory // created before olderThan. The list of files deleted and errors // encountered are returned func PurgeUploads(ctx context.Context, driver storageDriver.StorageDriver, olderThan time.Time, actuallyDelete bool) ([]string, []error) { log.Infof("PurgeUploads starting: olderThan=%s, actuallyDelete=%t", olderThan, actuallyDelete) uploadData, errors := getOutstandingUploads(ctx, driver) var deleted []string for _, uploadData := range uploadData { if uploadData.startedAt.Before(olderThan) { var err error log.Infof("Upload files in %s have older date (%s) than purge date (%s). Removing upload directory.", uploadData.containingDir, uploadData.startedAt, olderThan) if actuallyDelete { err = driver.Delete(ctx, uploadData.containingDir) } if err == nil { deleted = append(deleted, uploadData.containingDir) } else { errors = append(errors, err) } } } log.Infof("Purge uploads finished. Num deleted=%d, num errors=%d", len(deleted), len(errors)) return deleted, errors } // getOutstandingUploads walks the upload directory, collecting files // which could be eligible for deletion. The only reliable way to // classify the age of a file is with the date stored in the startedAt // file, so gather files by UUID with a date from startedAt. func getOutstandingUploads(ctx context.Context, driver storageDriver.StorageDriver) (map[string]uploadData, []error) { var errors []error uploads := make(map[string]uploadData, 0) inUploadDir := false root, err := pathFor(repositoriesRootPathSpec{}) if err != nil { return uploads, append(errors, err) } err = Walk(ctx, driver, root, func(fileInfo storageDriver.FileInfo) error { filePath := fileInfo.Path() _, file := path.Split(filePath) if file[0] == '_' { // Reserved directory inUploadDir = (file == "_uploads") if fileInfo.IsDir() && !inUploadDir { return ErrSkipDir } } uuid, isContainingDir := uUIDFromPath(filePath) if uuid == "" { // Cannot reliably delete return nil } ud, ok := uploads[uuid] if !ok { ud = newUploadData() } if isContainingDir { ud.containingDir = filePath } if file == "startedat" { if t, err := readStartedAtFile(driver, filePath); err == nil { ud.startedAt = t } else { errors = pushError(errors, filePath, err) } } uploads[uuid] = ud return nil }) if err != nil { errors = pushError(errors, root, err) } return uploads, errors } // uUIDFromPath extracts the upload UUID from a given path // If the UUID is the last path component, this is the containing // directory for all upload files func uUIDFromPath(path string) (string, bool) { components := strings.Split(path, "/") for i := len(components) - 1; i >= 0; i-- { if u, err := uuid.Parse(components[i]); err == nil { return u.String(), i == len(components)-1 } } return "", false } // readStartedAtFile reads the date from an upload's startedAtFile func readStartedAtFile(driver storageDriver.StorageDriver, path string) (time.Time, error) { // todo:(richardscothern) - pass in a context startedAtBytes, err := driver.GetContent(context.Background(), path) if err != nil { return time.Now(), err } startedAt, err := time.Parse(time.RFC3339, string(startedAtBytes)) if err != nil { return time.Now(), err } return startedAt, nil } docker-registry-2.6.2~ds1/registry/storage/purgeuploads_test.go000066400000000000000000000113061313450123100250520ustar00rootroot00000000000000package storage import ( "path" "strings" "testing" "time" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/inmemory" "github.com/docker/distribution/uuid" ) func testUploadFS(t *testing.T, numUploads int, repoName string, startedAt time.Time) (driver.StorageDriver, context.Context) { d := inmemory.New() ctx := context.Background() for i := 0; i < numUploads; i++ { addUploads(ctx, t, d, uuid.Generate().String(), repoName, startedAt) } return d, ctx } func addUploads(ctx context.Context, t *testing.T, d driver.StorageDriver, uploadID, repo string, startedAt time.Time) { dataPath, err := pathFor(uploadDataPathSpec{name: repo, id: uploadID}) if err != nil { t.Fatalf("Unable to resolve path") } if err := d.PutContent(ctx, dataPath, []byte("")); err != nil { t.Fatalf("Unable to write data file") } startedAtPath, err := pathFor(uploadStartedAtPathSpec{name: repo, id: uploadID}) if err != nil { t.Fatalf("Unable to resolve path") } if d.PutContent(ctx, startedAtPath, []byte(startedAt.Format(time.RFC3339))); err != nil { t.Fatalf("Unable to write startedAt file") } } func TestPurgeGather(t *testing.T) { uploadCount := 5 fs, ctx := testUploadFS(t, uploadCount, "test-repo", time.Now()) uploadData, errs := getOutstandingUploads(ctx, fs) if len(errs) != 0 { t.Errorf("Unexepected errors: %q", errs) } if len(uploadData) != uploadCount { t.Errorf("Unexpected upload file count: %d != %d", uploadCount, len(uploadData)) } } func TestPurgeNone(t *testing.T) { fs, ctx := testUploadFS(t, 10, "test-repo", time.Now()) oneHourAgo := time.Now().Add(-1 * time.Hour) deleted, errs := PurgeUploads(ctx, fs, oneHourAgo, true) if len(errs) != 0 { t.Error("Unexpected errors", errs) } if len(deleted) != 0 { t.Errorf("Unexpectedly deleted files for time: %s", oneHourAgo) } } func TestPurgeAll(t *testing.T) { uploadCount := 10 oneHourAgo := time.Now().Add(-1 * time.Hour) fs, ctx := testUploadFS(t, uploadCount, "test-repo", oneHourAgo) // Ensure > 1 repos are purged addUploads(ctx, t, fs, uuid.Generate().String(), "test-repo2", oneHourAgo) uploadCount++ deleted, errs := PurgeUploads(ctx, fs, time.Now(), true) if len(errs) != 0 { t.Error("Unexpected errors:", errs) } fileCount := uploadCount if len(deleted) != fileCount { t.Errorf("Unexpectedly deleted file count %d != %d", len(deleted), fileCount) } } func TestPurgeSome(t *testing.T) { oldUploadCount := 5 oneHourAgo := time.Now().Add(-1 * time.Hour) fs, ctx := testUploadFS(t, oldUploadCount, "library/test-repo", oneHourAgo) newUploadCount := 4 for i := 0; i < newUploadCount; i++ { addUploads(ctx, t, fs, uuid.Generate().String(), "test-repo", time.Now().Add(1*time.Hour)) } deleted, errs := PurgeUploads(ctx, fs, time.Now(), true) if len(errs) != 0 { t.Error("Unexpected errors:", errs) } if len(deleted) != oldUploadCount { t.Errorf("Unexpectedly deleted file count %d != %d", len(deleted), oldUploadCount) } } func TestPurgeOnlyUploads(t *testing.T) { oldUploadCount := 5 oneHourAgo := time.Now().Add(-1 * time.Hour) fs, ctx := testUploadFS(t, oldUploadCount, "test-repo", oneHourAgo) // Create a directory tree outside _uploads and ensure // these files aren't deleted. dataPath, err := pathFor(uploadDataPathSpec{name: "test-repo", id: uuid.Generate().String()}) if err != nil { t.Fatalf(err.Error()) } nonUploadPath := strings.Replace(dataPath, "_upload", "_important", -1) if strings.Index(nonUploadPath, "_upload") != -1 { t.Fatalf("Non-upload path not created correctly") } nonUploadFile := path.Join(nonUploadPath, "file") if err = fs.PutContent(ctx, nonUploadFile, []byte("")); err != nil { t.Fatalf("Unable to write data file") } deleted, errs := PurgeUploads(ctx, fs, time.Now(), true) if len(errs) != 0 { t.Error("Unexpected errors", errs) } for _, file := range deleted { if strings.Index(file, "_upload") == -1 { t.Errorf("Non-upload file deleted") } } } func TestPurgeMissingStartedAt(t *testing.T) { oneHourAgo := time.Now().Add(-1 * time.Hour) fs, ctx := testUploadFS(t, 1, "test-repo", oneHourAgo) err := Walk(ctx, fs, "/", func(fileInfo driver.FileInfo) error { filePath := fileInfo.Path() _, file := path.Split(filePath) if file == "startedat" { if err := fs.Delete(ctx, filePath); err != nil { t.Fatalf("Unable to delete startedat file: %s", filePath) } } return nil }) if err != nil { t.Fatalf("Unexpected error during Walk: %s ", err.Error()) } deleted, errs := PurgeUploads(ctx, fs, time.Now(), true) if len(errs) > 0 { t.Errorf("Unexpected errors") } if len(deleted) > 0 { t.Errorf("Files unexpectedly deleted: %s", deleted) } } docker-registry-2.6.2~ds1/registry/storage/registry.go000066400000000000000000000226711313450123100231600ustar00rootroot00000000000000package storage import ( "regexp" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/cache" storagedriver "github.com/docker/distribution/registry/storage/driver" "github.com/docker/libtrust" ) // registry is the top-level implementation of Registry for use in the storage // package. All instances should descend from this object. type registry struct { blobStore *blobStore blobServer *blobServer statter *blobStatter // global statter service. blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider deleteEnabled bool resumableDigestEnabled bool schema1SigningKey libtrust.PrivateKey blobDescriptorServiceFactory distribution.BlobDescriptorServiceFactory manifestURLs manifestURLs } // manifestURLs holds regular expressions for controlling manifest URL whitelisting type manifestURLs struct { allow *regexp.Regexp deny *regexp.Regexp } // RegistryOption is the type used for functional options for NewRegistry. type RegistryOption func(*registry) error // EnableRedirect is a functional option for NewRegistry. It causes the backend // blob server to attempt using (StorageDriver).URLFor to serve all blobs. func EnableRedirect(registry *registry) error { registry.blobServer.redirect = true return nil } // EnableDelete is a functional option for NewRegistry. It enables deletion on // the registry. func EnableDelete(registry *registry) error { registry.deleteEnabled = true return nil } // DisableDigestResumption is a functional option for NewRegistry. It should be // used if the registry is acting as a caching proxy. func DisableDigestResumption(registry *registry) error { registry.resumableDigestEnabled = false return nil } // ManifestURLsAllowRegexp is a functional option for NewRegistry. func ManifestURLsAllowRegexp(r *regexp.Regexp) RegistryOption { return func(registry *registry) error { registry.manifestURLs.allow = r return nil } } // ManifestURLsDenyRegexp is a functional option for NewRegistry. func ManifestURLsDenyRegexp(r *regexp.Regexp) RegistryOption { return func(registry *registry) error { registry.manifestURLs.deny = r return nil } } // Schema1SigningKey returns a functional option for NewRegistry. It sets the // key for signing all schema1 manifests. func Schema1SigningKey(key libtrust.PrivateKey) RegistryOption { return func(registry *registry) error { registry.schema1SigningKey = key return nil } } // BlobDescriptorServiceFactory returns a functional option for NewRegistry. It sets the // factory to create BlobDescriptorServiceFactory middleware. func BlobDescriptorServiceFactory(factory distribution.BlobDescriptorServiceFactory) RegistryOption { return func(registry *registry) error { registry.blobDescriptorServiceFactory = factory return nil } } // BlobDescriptorCacheProvider returns a functional option for // NewRegistry. It creates a cached blob statter for use by the // registry. func BlobDescriptorCacheProvider(blobDescriptorCacheProvider cache.BlobDescriptorCacheProvider) RegistryOption { // TODO(aaronl): The duplication of statter across several objects is // ugly, and prevents us from using interface types in the registry // struct. Ideally, blobStore and blobServer should be lazily // initialized, and use the current value of // blobDescriptorCacheProvider. return func(registry *registry) error { if blobDescriptorCacheProvider != nil { statter := cache.NewCachedBlobStatter(blobDescriptorCacheProvider, registry.statter) registry.blobStore.statter = statter registry.blobServer.statter = statter registry.blobDescriptorCacheProvider = blobDescriptorCacheProvider } return nil } } // NewRegistry creates a new registry instance from the provided driver. The // resulting registry may be shared by multiple goroutines but is cheap to // allocate. If the Redirect option is specified, the backend blob server will // attempt to use (StorageDriver).URLFor to serve all blobs. func NewRegistry(ctx context.Context, driver storagedriver.StorageDriver, options ...RegistryOption) (distribution.Namespace, error) { // create global statter statter := &blobStatter{ driver: driver, } bs := &blobStore{ driver: driver, statter: statter, } registry := ®istry{ blobStore: bs, blobServer: &blobServer{ driver: driver, statter: statter, pathFn: bs.path, }, statter: statter, resumableDigestEnabled: true, } for _, option := range options { if err := option(registry); err != nil { return nil, err } } return registry, nil } // Scope returns the namespace scope for a registry. The registry // will only serve repositories contained within this scope. func (reg *registry) Scope() distribution.Scope { return distribution.GlobalScope } // Repository returns an instance of the repository tied to the registry. // Instances should not be shared between goroutines but are cheap to // allocate. In general, they should be request scoped. func (reg *registry) Repository(ctx context.Context, canonicalName reference.Named) (distribution.Repository, error) { var descriptorCache distribution.BlobDescriptorService if reg.blobDescriptorCacheProvider != nil { var err error descriptorCache, err = reg.blobDescriptorCacheProvider.RepositoryScoped(canonicalName.Name()) if err != nil { return nil, err } } return &repository{ ctx: ctx, registry: reg, name: canonicalName, descriptorCache: descriptorCache, }, nil } func (reg *registry) Blobs() distribution.BlobEnumerator { return reg.blobStore } func (reg *registry) BlobStatter() distribution.BlobStatter { return reg.statter } // repository provides name-scoped access to various services. type repository struct { *registry ctx context.Context name reference.Named descriptorCache distribution.BlobDescriptorService } // Name returns the name of the repository. func (repo *repository) Named() reference.Named { return repo.name } func (repo *repository) Tags(ctx context.Context) distribution.TagService { tags := &tagStore{ repository: repo, blobStore: repo.registry.blobStore, } return tags } // Manifests returns an instance of ManifestService. Instantiation is cheap and // may be context sensitive in the future. The instance should be used similar // to a request local. func (repo *repository) Manifests(ctx context.Context, options ...distribution.ManifestServiceOption) (distribution.ManifestService, error) { manifestLinkPathFns := []linkPathFunc{ // NOTE(stevvooe): Need to search through multiple locations since // 2.1.0 unintentionally linked into _layers. manifestRevisionLinkPath, blobLinkPath, } manifestDirectoryPathSpec := manifestRevisionsPathSpec{name: repo.name.Name()} var statter distribution.BlobDescriptorService = &linkedBlobStatter{ blobStore: repo.blobStore, repository: repo, linkPathFns: manifestLinkPathFns, } if repo.registry.blobDescriptorServiceFactory != nil { statter = repo.registry.blobDescriptorServiceFactory.BlobAccessController(statter) } blobStore := &linkedBlobStore{ ctx: ctx, blobStore: repo.blobStore, repository: repo, deleteEnabled: repo.registry.deleteEnabled, blobAccessController: statter, // TODO(stevvooe): linkPath limits this blob store to only // manifests. This instance cannot be used for blob checks. linkPathFns: manifestLinkPathFns, linkDirectoryPathSpec: manifestDirectoryPathSpec, } ms := &manifestStore{ ctx: ctx, repository: repo, blobStore: blobStore, schema1Handler: &signedManifestHandler{ ctx: ctx, schema1SigningKey: repo.schema1SigningKey, repository: repo, blobStore: blobStore, }, schema2Handler: &schema2ManifestHandler{ ctx: ctx, repository: repo, blobStore: blobStore, manifestURLs: repo.registry.manifestURLs, }, manifestListHandler: &manifestListHandler{ ctx: ctx, repository: repo, blobStore: blobStore, }, } // Apply options for _, option := range options { err := option.Apply(ms) if err != nil { return nil, err } } return ms, nil } // Blobs returns an instance of the BlobStore. Instantiation is cheap and // may be context sensitive in the future. The instance should be used similar // to a request local. func (repo *repository) Blobs(ctx context.Context) distribution.BlobStore { var statter distribution.BlobDescriptorService = &linkedBlobStatter{ blobStore: repo.blobStore, repository: repo, linkPathFns: []linkPathFunc{blobLinkPath}, } if repo.descriptorCache != nil { statter = cache.NewCachedBlobStatter(repo.descriptorCache, statter) } if repo.registry.blobDescriptorServiceFactory != nil { statter = repo.registry.blobDescriptorServiceFactory.BlobAccessController(statter) } return &linkedBlobStore{ registry: repo.registry, blobStore: repo.blobStore, blobServer: repo.blobServer, blobAccessController: statter, repository: repo, ctx: ctx, // TODO(stevvooe): linkPath limits this blob store to only layers. // This instance cannot be used for manifest checks. linkPathFns: []linkPathFunc{blobLinkPath}, deleteEnabled: repo.registry.deleteEnabled, resumableDigestEnabled: repo.resumableDigestEnabled, } } docker-registry-2.6.2~ds1/registry/storage/schema2manifesthandler.go000066400000000000000000000074351313450123100257200ustar00rootroot00000000000000package storage import ( "encoding/json" "errors" "fmt" "net/url" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" ) var ( errUnexpectedURL = errors.New("unexpected URL on layer") errMissingURL = errors.New("missing URL on layer") errInvalidURL = errors.New("invalid URL on layer") ) //schema2ManifestHandler is a ManifestHandler that covers schema2 manifests. type schema2ManifestHandler struct { repository distribution.Repository blobStore distribution.BlobStore ctx context.Context manifestURLs manifestURLs } var _ ManifestHandler = &schema2ManifestHandler{} func (ms *schema2ManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Unmarshal") var m schema2.DeserializedManifest if err := json.Unmarshal(content, &m); err != nil { return nil, err } return &m, nil } func (ms *schema2ManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { context.GetLogger(ms.ctx).Debug("(*schema2ManifestHandler).Put") m, ok := manifest.(*schema2.DeserializedManifest) if !ok { return "", fmt.Errorf("non-schema2 manifest put to schema2ManifestHandler: %T", manifest) } if err := ms.verifyManifest(ms.ctx, *m, skipDependencyVerification); err != nil { return "", err } mt, payload, err := m.Payload() if err != nil { return "", err } revision, err := ms.blobStore.Put(ctx, mt, payload) if err != nil { context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) return "", err } return revision.Digest, nil } // verifyManifest ensures that the manifest content is valid from the // perspective of the registry. As a policy, the registry only tries to store // valid content, leaving trust policies of that content up to consumers. func (ms *schema2ManifestHandler) verifyManifest(ctx context.Context, mnfst schema2.DeserializedManifest, skipDependencyVerification bool) error { var errs distribution.ErrManifestVerification if skipDependencyVerification { return nil } manifestService, err := ms.repository.Manifests(ctx) if err != nil { return err } blobsService := ms.repository.Blobs(ctx) for _, descriptor := range mnfst.References() { var err error switch descriptor.MediaType { case schema2.MediaTypeForeignLayer: // Clients download this layer from an external URL, so do not check for // its presense. if len(descriptor.URLs) == 0 { err = errMissingURL } allow := ms.manifestURLs.allow deny := ms.manifestURLs.deny for _, u := range descriptor.URLs { var pu *url.URL pu, err = url.Parse(u) if err != nil || (pu.Scheme != "http" && pu.Scheme != "https") || pu.Fragment != "" || (allow != nil && !allow.MatchString(u)) || (deny != nil && deny.MatchString(u)) { err = errInvalidURL break } } case schema2.MediaTypeManifest, schema1.MediaTypeManifest: var exists bool exists, err = manifestService.Exists(ctx, descriptor.Digest) if err != nil || !exists { err = distribution.ErrBlobUnknown // just coerce to unknown. } fallthrough // double check the blob store. default: // forward all else to blob storage if len(descriptor.URLs) == 0 { _, err = blobsService.Stat(ctx, descriptor.Digest) } } if err != nil { if err != distribution.ErrBlobUnknown { errs = append(errs, err) } // On error here, we always append unknown blob errors. errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: descriptor.Digest}) } } if len(errs) != 0 { return errs } return nil } docker-registry-2.6.2~ds1/registry/storage/schema2manifesthandler_test.go000066400000000000000000000052631313450123100267540ustar00rootroot00000000000000package storage import ( "regexp" "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/registry/storage/driver/inmemory" ) func TestVerifyManifestForeignLayer(t *testing.T) { ctx := context.Background() inmemoryDriver := inmemory.New() registry := createRegistry(t, inmemoryDriver, ManifestURLsAllowRegexp(regexp.MustCompile("^https?://foo")), ManifestURLsDenyRegexp(regexp.MustCompile("^https?://foo/nope"))) repo := makeRepository(t, registry, "test") manifestService := makeManifestService(t, repo) config, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeConfig, nil) if err != nil { t.Fatal(err) } layer, err := repo.Blobs(ctx).Put(ctx, schema2.MediaTypeLayer, nil) if err != nil { t.Fatal(err) } foreignLayer := distribution.Descriptor{ Digest: "sha256:463435349086340864309863409683460843608348608934092322395278926a", Size: 6323, MediaType: schema2.MediaTypeForeignLayer, } template := schema2.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 2, MediaType: schema2.MediaTypeManifest, }, Config: config, } type testcase struct { BaseLayer distribution.Descriptor URLs []string Err error } cases := []testcase{ { foreignLayer, nil, errMissingURL, }, { // regular layers may have foreign urls layer, []string{"http://foo/bar"}, nil, }, { foreignLayer, []string{"file:///local/file"}, errInvalidURL, }, { foreignLayer, []string{"http://foo/bar#baz"}, errInvalidURL, }, { foreignLayer, []string{""}, errInvalidURL, }, { foreignLayer, []string{"https://foo/bar", ""}, errInvalidURL, }, { foreignLayer, []string{"", "https://foo/bar"}, errInvalidURL, }, { foreignLayer, []string{"http://nope/bar"}, errInvalidURL, }, { foreignLayer, []string{"http://foo/nope"}, errInvalidURL, }, { foreignLayer, []string{"http://foo/bar"}, nil, }, { foreignLayer, []string{"https://foo/bar"}, nil, }, } for _, c := range cases { m := template l := c.BaseLayer l.URLs = c.URLs m.Layers = []distribution.Descriptor{l} dm, err := schema2.FromStruct(m) if err != nil { t.Error(err) continue } _, err = manifestService.Put(ctx, dm) if verr, ok := err.(distribution.ErrManifestVerification); ok { // Extract the first error if len(verr) == 2 { if _, ok = verr[1].(distribution.ErrManifestBlobUnknown); ok { err = verr[0] } } } if err != c.Err { t.Errorf("%#v: expected %v, got %v", l, c.Err, err) } } } docker-registry-2.6.2~ds1/registry/storage/signedmanifesthandler.go000066400000000000000000000101311313450123100256320ustar00rootroot00000000000000package storage import ( "encoding/json" "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/reference" "github.com/docker/libtrust" ) // signedManifestHandler is a ManifestHandler that covers schema1 manifests. It // can unmarshal and put schema1 manifests that have been signed by libtrust. type signedManifestHandler struct { repository distribution.Repository schema1SigningKey libtrust.PrivateKey blobStore distribution.BlobStore ctx context.Context } var _ ManifestHandler = &signedManifestHandler{} func (ms *signedManifestHandler) Unmarshal(ctx context.Context, dgst digest.Digest, content []byte) (distribution.Manifest, error) { context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Unmarshal") var ( signatures [][]byte err error ) jsig, err := libtrust.NewJSONSignature(content, signatures...) if err != nil { return nil, err } if ms.schema1SigningKey != nil { if err := jsig.Sign(ms.schema1SigningKey); err != nil { return nil, err } } // Extract the pretty JWS raw, err := jsig.PrettySignature("signatures") if err != nil { return nil, err } var sm schema1.SignedManifest if err := json.Unmarshal(raw, &sm); err != nil { return nil, err } return &sm, nil } func (ms *signedManifestHandler) Put(ctx context.Context, manifest distribution.Manifest, skipDependencyVerification bool) (digest.Digest, error) { context.GetLogger(ms.ctx).Debug("(*signedManifestHandler).Put") sm, ok := manifest.(*schema1.SignedManifest) if !ok { return "", fmt.Errorf("non-schema1 manifest put to signedManifestHandler: %T", manifest) } if err := ms.verifyManifest(ms.ctx, *sm, skipDependencyVerification); err != nil { return "", err } mt := schema1.MediaTypeManifest payload := sm.Canonical revision, err := ms.blobStore.Put(ctx, mt, payload) if err != nil { context.GetLogger(ctx).Errorf("error putting payload into blobstore: %v", err) return "", err } return revision.Digest, nil } // verifyManifest ensures that the manifest content is valid from the // perspective of the registry. It ensures that the signature is valid for the // enclosed payload. As a policy, the registry only tries to store valid // content, leaving trust policies of that content up to consumers. func (ms *signedManifestHandler) verifyManifest(ctx context.Context, mnfst schema1.SignedManifest, skipDependencyVerification bool) error { var errs distribution.ErrManifestVerification if len(mnfst.Name) > reference.NameTotalLengthMax { errs = append(errs, distribution.ErrManifestNameInvalid{ Name: mnfst.Name, Reason: fmt.Errorf("manifest name must not be more than %v characters", reference.NameTotalLengthMax), }) } if !reference.NameRegexp.MatchString(mnfst.Name) { errs = append(errs, distribution.ErrManifestNameInvalid{ Name: mnfst.Name, Reason: fmt.Errorf("invalid manifest name format"), }) } if len(mnfst.History) != len(mnfst.FSLayers) { errs = append(errs, fmt.Errorf("mismatched history and fslayer cardinality %d != %d", len(mnfst.History), len(mnfst.FSLayers))) } if _, err := schema1.Verify(&mnfst); err != nil { switch err { case libtrust.ErrMissingSignatureKey, libtrust.ErrInvalidJSONContent, libtrust.ErrMissingSignatureKey: errs = append(errs, distribution.ErrManifestUnverified{}) default: if err.Error() == "invalid signature" { // TODO(stevvooe): This should be exported by libtrust errs = append(errs, distribution.ErrManifestUnverified{}) } else { errs = append(errs, err) } } } if !skipDependencyVerification { for _, fsLayer := range mnfst.References() { _, err := ms.repository.Blobs(ctx).Stat(ctx, fsLayer.Digest) if err != nil { if err != distribution.ErrBlobUnknown { errs = append(errs, err) } // On error here, we always append unknown blob errors. errs = append(errs, distribution.ErrManifestBlobUnknown{Digest: fsLayer.Digest}) } } } if len(errs) != 0 { return errs } return nil } docker-registry-2.6.2~ds1/registry/storage/tagstore.go000066400000000000000000000115651313450123100231400ustar00rootroot00000000000000package storage import ( "path" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" storagedriver "github.com/docker/distribution/registry/storage/driver" ) var _ distribution.TagService = &tagStore{} // tagStore provides methods to manage manifest tags in a backend storage driver. // This implementation uses the same on-disk layout as the (now deleted) tag // store. This provides backward compatibility with current registry deployments // which only makes use of the Digest field of the returned distribution.Descriptor // but does not enable full roundtripping of Descriptor objects type tagStore struct { repository *repository blobStore *blobStore } // All returns all tags func (ts *tagStore) All(ctx context.Context) ([]string, error) { var tags []string pathSpec, err := pathFor(manifestTagPathSpec{ name: ts.repository.Named().Name(), }) if err != nil { return tags, err } entries, err := ts.blobStore.driver.List(ctx, pathSpec) if err != nil { switch err := err.(type) { case storagedriver.PathNotFoundError: return tags, distribution.ErrRepositoryUnknown{Name: ts.repository.Named().Name()} default: return tags, err } } for _, entry := range entries { _, filename := path.Split(entry) tags = append(tags, filename) } return tags, nil } // exists returns true if the specified manifest tag exists in the repository. func (ts *tagStore) exists(ctx context.Context, tag string) (bool, error) { tagPath, err := pathFor(manifestTagCurrentPathSpec{ name: ts.repository.Named().Name(), tag: tag, }) if err != nil { return false, err } exists, err := exists(ctx, ts.blobStore.driver, tagPath) if err != nil { return false, err } return exists, nil } // Tag tags the digest with the given tag, updating the the store to point at // the current tag. The digest must point to a manifest. func (ts *tagStore) Tag(ctx context.Context, tag string, desc distribution.Descriptor) error { currentPath, err := pathFor(manifestTagCurrentPathSpec{ name: ts.repository.Named().Name(), tag: tag, }) if err != nil { return err } lbs := ts.linkedBlobStore(ctx, tag) // Link into the index if err := lbs.linkBlob(ctx, desc); err != nil { return err } // Overwrite the current link return ts.blobStore.link(ctx, currentPath, desc.Digest) } // resolve the current revision for name and tag. func (ts *tagStore) Get(ctx context.Context, tag string) (distribution.Descriptor, error) { currentPath, err := pathFor(manifestTagCurrentPathSpec{ name: ts.repository.Named().Name(), tag: tag, }) if err != nil { return distribution.Descriptor{}, err } revision, err := ts.blobStore.readlink(ctx, currentPath) if err != nil { switch err.(type) { case storagedriver.PathNotFoundError: return distribution.Descriptor{}, distribution.ErrTagUnknown{Tag: tag} } return distribution.Descriptor{}, err } return distribution.Descriptor{Digest: revision}, nil } // Untag removes the tag association func (ts *tagStore) Untag(ctx context.Context, tag string) error { tagPath, err := pathFor(manifestTagPathSpec{ name: ts.repository.Named().Name(), tag: tag, }) switch err.(type) { case storagedriver.PathNotFoundError: return distribution.ErrTagUnknown{Tag: tag} case nil: break default: return err } return ts.blobStore.driver.Delete(ctx, tagPath) } // linkedBlobStore returns the linkedBlobStore for the named tag, allowing one // to index manifest blobs by tag name. While the tag store doesn't map // precisely to the linked blob store, using this ensures the links are // managed via the same code path. func (ts *tagStore) linkedBlobStore(ctx context.Context, tag string) *linkedBlobStore { return &linkedBlobStore{ blobStore: ts.blobStore, repository: ts.repository, ctx: ctx, linkPathFns: []linkPathFunc{func(name string, dgst digest.Digest) (string, error) { return pathFor(manifestTagIndexEntryLinkPathSpec{ name: name, tag: tag, revision: dgst, }) }}, } } // Lookup recovers a list of tags which refer to this digest. When a manifest is deleted by // digest, tag entries which point to it need to be recovered to avoid dangling tags. func (ts *tagStore) Lookup(ctx context.Context, desc distribution.Descriptor) ([]string, error) { allTags, err := ts.All(ctx) switch err.(type) { case distribution.ErrRepositoryUnknown: // This tag store has been initialized but not yet populated break case nil: break default: return nil, err } var tags []string for _, tag := range allTags { tagLinkPathSpec := manifestTagCurrentPathSpec{ name: ts.repository.Named().Name(), tag: tag, } tagLinkPath, err := pathFor(tagLinkPathSpec) tagDigest, err := ts.blobStore.readlink(ctx, tagLinkPath) if err != nil { return nil, err } if tagDigest == desc.Digest { tags = append(tags, tag) } } return tags, nil } docker-registry-2.6.2~ds1/registry/storage/tagstore_test.go000066400000000000000000000102321313450123100241650ustar00rootroot00000000000000package storage import ( "testing" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/reference" "github.com/docker/distribution/registry/storage/driver/inmemory" ) type tagsTestEnv struct { ts distribution.TagService ctx context.Context } func testTagStore(t *testing.T) *tagsTestEnv { ctx := context.Background() d := inmemory.New() reg, err := NewRegistry(ctx, d) if err != nil { t.Fatal(err) } repoRef, _ := reference.ParseNamed("a/b") repo, err := reg.Repository(ctx, repoRef) if err != nil { t.Fatal(err) } return &tagsTestEnv{ ctx: ctx, ts: repo.Tags(ctx), } } func TestTagStoreTag(t *testing.T) { env := testTagStore(t) tags := env.ts ctx := env.ctx d := distribution.Descriptor{} err := tags.Tag(ctx, "latest", d) if err == nil { t.Errorf("unexpected error putting malformed descriptor : %s", err) } d.Digest = "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" err = tags.Tag(ctx, "latest", d) if err != nil { t.Error(err) } d1, err := tags.Get(ctx, "latest") if err != nil { t.Error(err) } if d1.Digest != d.Digest { t.Error("put and get digest differ") } // Overwrite existing d.Digest = "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" err = tags.Tag(ctx, "latest", d) if err != nil { t.Error(err) } d1, err = tags.Get(ctx, "latest") if err != nil { t.Error(err) } if d1.Digest != d.Digest { t.Error("put and get digest differ") } } func TestTagStoreUnTag(t *testing.T) { env := testTagStore(t) tags := env.ts ctx := env.ctx desc := distribution.Descriptor{Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"} err := tags.Untag(ctx, "latest") if err == nil { t.Errorf("Expected error untagging non-existant tag") } err = tags.Tag(ctx, "latest", desc) if err != nil { t.Error(err) } err = tags.Untag(ctx, "latest") if err != nil { t.Error(err) } errExpect := distribution.ErrTagUnknown{Tag: "latest"}.Error() _, err = tags.Get(ctx, "latest") if err == nil || err.Error() != errExpect { t.Error("Expected error getting untagged tag") } } func TestTagStoreAll(t *testing.T) { env := testTagStore(t) tagStore := env.ts ctx := env.ctx alpha := "abcdefghijklmnopqrstuvwxyz" for i := 0; i < len(alpha); i++ { tag := alpha[i] desc := distribution.Descriptor{Digest: "sha256:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"} err := tagStore.Tag(ctx, string(tag), desc) if err != nil { t.Error(err) } } all, err := tagStore.All(ctx) if err != nil { t.Error(err) } if len(all) != len(alpha) { t.Errorf("Unexpected count returned from enumerate") } for i, c := range all { if c != string(alpha[i]) { t.Errorf("unexpected tag in enumerate %s", c) } } removed := "a" err = tagStore.Untag(ctx, removed) if err != nil { t.Error(err) } all, err = tagStore.All(ctx) if err != nil { t.Error(err) } for _, tag := range all { if tag == removed { t.Errorf("unexpected tag in enumerate %s", removed) } } } func TestTagLookup(t *testing.T) { env := testTagStore(t) tagStore := env.ts ctx := env.ctx descA := distribution.Descriptor{Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"} desc0 := distribution.Descriptor{Digest: "sha256:0000000000000000000000000000000000000000000000000000000000000000"} tags, err := tagStore.Lookup(ctx, descA) if err != nil { t.Fatal(err) } if len(tags) != 0 { t.Fatalf("Lookup returned > 0 tags from empty store") } err = tagStore.Tag(ctx, "a", descA) if err != nil { t.Fatal(err) } err = tagStore.Tag(ctx, "b", descA) if err != nil { t.Fatal(err) } err = tagStore.Tag(ctx, "0", desc0) if err != nil { t.Fatal(err) } err = tagStore.Tag(ctx, "1", desc0) if err != nil { t.Fatal(err) } tags, err = tagStore.Lookup(ctx, descA) if err != nil { t.Fatal(err) } if len(tags) != 2 { t.Errorf("Lookup of descA returned %d tags, expected 2", len(tags)) } tags, err = tagStore.Lookup(ctx, desc0) if err != nil { t.Fatal(err) } if len(tags) != 2 { t.Errorf("Lookup of descB returned %d tags, expected 2", len(tags)) } } docker-registry-2.6.2~ds1/registry/storage/util.go000066400000000000000000000007711313450123100222620ustar00rootroot00000000000000package storage import ( "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage/driver" ) // Exists provides a utility method to test whether or not a path exists in // the given driver. func exists(ctx context.Context, drv driver.StorageDriver, path string) (bool, error) { if _, err := drv.Stat(ctx, path); err != nil { switch err := err.(type) { case driver.PathNotFoundError: return false, nil default: return false, err } } return true, nil } docker-registry-2.6.2~ds1/registry/storage/vacuum.go000066400000000000000000000027401313450123100226030ustar00rootroot00000000000000package storage import ( "path" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/registry/storage/driver" ) // vacuum contains functions for cleaning up repositories and blobs // These functions will only reliably work on strongly consistent // storage systems. // https://en.wikipedia.org/wiki/Consistency_model // NewVacuum creates a new Vacuum func NewVacuum(ctx context.Context, driver driver.StorageDriver) Vacuum { return Vacuum{ ctx: ctx, driver: driver, } } // Vacuum removes content from the filesystem type Vacuum struct { driver driver.StorageDriver ctx context.Context } // RemoveBlob removes a blob from the filesystem func (v Vacuum) RemoveBlob(dgst string) error { d, err := digest.ParseDigest(dgst) if err != nil { return err } blobPath, err := pathFor(blobPathSpec{digest: d}) if err != nil { return err } context.GetLogger(v.ctx).Infof("Deleting blob: %s", blobPath) err = v.driver.Delete(v.ctx, blobPath) if err != nil { return err } return nil } // RemoveRepository removes a repository directory from the // filesystem func (v Vacuum) RemoveRepository(repoName string) error { rootForRepository, err := pathFor(repositoriesRootPathSpec{}) if err != nil { return err } repoDir := path.Join(rootForRepository, repoName) context.GetLogger(v.ctx).Infof("Deleting repo: %s", repoDir) err = v.driver.Delete(v.ctx, repoDir) if err != nil { return err } return nil } docker-registry-2.6.2~ds1/registry/storage/walk.go000066400000000000000000000033771313450123100222500ustar00rootroot00000000000000package storage import ( "errors" "fmt" "sort" "github.com/docker/distribution/context" storageDriver "github.com/docker/distribution/registry/storage/driver" ) // ErrSkipDir is used as a return value from onFileFunc to indicate that // the directory named in the call is to be skipped. It is not returned // as an error by any function. var ErrSkipDir = errors.New("skip this directory") // WalkFn is called once per file by Walk // If the returned error is ErrSkipDir and fileInfo refers // to a directory, the directory will not be entered and Walk // will continue the traversal. Otherwise Walk will return type WalkFn func(fileInfo storageDriver.FileInfo) error // Walk traverses a filesystem defined within driver, starting // from the given path, calling f on each file func Walk(ctx context.Context, driver storageDriver.StorageDriver, from string, f WalkFn) error { children, err := driver.List(ctx, from) if err != nil { return err } sort.Stable(sort.StringSlice(children)) for _, child := range children { // TODO(stevvooe): Calling driver.Stat for every entry is quite // expensive when running against backends with a slow Stat // implementation, such as s3. This is very likely a serious // performance bottleneck. fileInfo, err := driver.Stat(ctx, child) if err != nil { return err } err = f(fileInfo) skipDir := (err == ErrSkipDir) if err != nil && !skipDir { return err } if fileInfo.IsDir() && !skipDir { if err := Walk(ctx, driver, child, f); err != nil { return err } } } return nil } // pushError formats an error type given a path and an error // and pushes it to a slice of errors func pushError(errors []error, path string, err error) []error { return append(errors, fmt.Errorf("%s: %s", path, err)) } docker-registry-2.6.2~ds1/registry/storage/walk_test.go000066400000000000000000000065171313450123100233060ustar00rootroot00000000000000package storage import ( "fmt" "sort" "testing" "github.com/docker/distribution/context" "github.com/docker/distribution/registry/storage/driver" "github.com/docker/distribution/registry/storage/driver/inmemory" ) func testFS(t *testing.T) (driver.StorageDriver, map[string]string, context.Context) { d := inmemory.New() ctx := context.Background() expected := map[string]string{ "/a": "dir", "/a/b": "dir", "/a/b/c": "dir", "/a/b/c/d": "file", "/a/b/c/e": "file", "/a/b/f": "dir", "/a/b/f/g": "file", "/a/b/f/h": "file", "/a/b/f/i": "file", "/z": "dir", "/z/y": "file", } for p, typ := range expected { if typ != "file" { continue } if err := d.PutContent(ctx, p, []byte(p)); err != nil { t.Fatalf("unable to put content into fixture: %v", err) } } return d, expected, ctx } func TestWalkErrors(t *testing.T) { d, expected, ctx := testFS(t) fileCount := len(expected) err := Walk(ctx, d, "", func(fileInfo driver.FileInfo) error { return nil }) if err == nil { t.Error("Expected invalid root err") } errEarlyExpected := fmt.Errorf("Early termination") err = Walk(ctx, d, "/", func(fileInfo driver.FileInfo) error { // error on the 2nd file if fileInfo.Path() == "/a/b" { return errEarlyExpected } delete(expected, fileInfo.Path()) return nil }) if len(expected) != fileCount-1 { t.Error("Walk failed to terminate with error") } if err != errEarlyExpected { if err == nil { t.Fatalf("expected an error due to early termination") } else { t.Error(err.Error()) } } err = Walk(ctx, d, "/nonexistent", func(fileInfo driver.FileInfo) error { return nil }) if err == nil { t.Errorf("Expected missing file err") } } func TestWalk(t *testing.T) { d, expected, ctx := testFS(t) var traversed []string err := Walk(ctx, d, "/", func(fileInfo driver.FileInfo) error { filePath := fileInfo.Path() filetype, ok := expected[filePath] if !ok { t.Fatalf("Unexpected file in walk: %q", filePath) } if fileInfo.IsDir() { if filetype != "dir" { t.Errorf("Unexpected file type: %q", filePath) } } else { if filetype != "file" { t.Errorf("Unexpected file type: %q", filePath) } // each file has its own path as the contents. If the length // doesn't match the path length, fail. if fileInfo.Size() != int64(len(fileInfo.Path())) { t.Fatalf("unexpected size for %q: %v != %v", fileInfo.Path(), fileInfo.Size(), len(fileInfo.Path())) } } delete(expected, filePath) traversed = append(traversed, filePath) return nil }) if len(expected) > 0 { t.Errorf("Missed files in walk: %q", expected) } if !sort.StringsAreSorted(traversed) { t.Errorf("result should be sorted: %v", traversed) } if err != nil { t.Fatalf(err.Error()) } } func TestWalkSkipDir(t *testing.T) { d, expected, ctx := testFS(t) err := Walk(ctx, d, "/", func(fileInfo driver.FileInfo) error { filePath := fileInfo.Path() if filePath == "/a/b" { // skip processing /a/b/c and /a/b/c/d return ErrSkipDir } delete(expected, filePath) return nil }) if err != nil { t.Fatalf(err.Error()) } if _, ok := expected["/a/b/c"]; !ok { t.Errorf("/a/b/c not skipped") } if _, ok := expected["/a/b/c/d"]; !ok { t.Errorf("/a/b/c/d not skipped") } if _, ok := expected["/a/b/c/e"]; !ok { t.Errorf("/a/b/c/e not skipped") } } docker-registry-2.6.2~ds1/tags.go000066400000000000000000000017361313450123100167310ustar00rootroot00000000000000package distribution import ( "github.com/docker/distribution/context" ) // TagService provides access to information about tagged objects. type TagService interface { // Get retrieves the descriptor identified by the tag. Some // implementations may differentiate between "trusted" tags and // "untrusted" tags. If a tag is "untrusted", the mapping will be returned // as an ErrTagUntrusted error, with the target descriptor. Get(ctx context.Context, tag string) (Descriptor, error) // Tag associates the tag with the provided descriptor, updating the // current association, if needed. Tag(ctx context.Context, tag string, desc Descriptor) error // Untag removes the given tag association Untag(ctx context.Context, tag string) error // All returns the set of tags managed by this tag service All(ctx context.Context) ([]string, error) // Lookup returns the set of tags referencing the given digest. Lookup(ctx context.Context, digest Descriptor) ([]string, error) } docker-registry-2.6.2~ds1/testutil/000077500000000000000000000000001313450123100173125ustar00rootroot00000000000000docker-registry-2.6.2~ds1/testutil/handler.go000066400000000000000000000071331313450123100212620ustar00rootroot00000000000000package testutil import ( "bytes" "fmt" "io" "io/ioutil" "net/http" "net/url" "sort" "strings" ) // RequestResponseMap is an ordered mapping from Requests to Responses type RequestResponseMap []RequestResponseMapping // RequestResponseMapping defines a Response to be sent in response to a given // Request type RequestResponseMapping struct { Request Request Response Response } // Request is a simplified http.Request object type Request struct { // Method is the http method of the request, for example GET Method string // Route is the http route of this request Route string // QueryParams are the query parameters of this request QueryParams map[string][]string // Body is the byte contents of the http request Body []byte // Headers are the header for this request Headers http.Header } func (r Request) String() string { queryString := "" if len(r.QueryParams) > 0 { keys := make([]string, 0, len(r.QueryParams)) queryParts := make([]string, 0, len(r.QueryParams)) for k := range r.QueryParams { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { for _, val := range r.QueryParams[k] { queryParts = append(queryParts, fmt.Sprintf("%s=%s", k, url.QueryEscape(val))) } } queryString = "?" + strings.Join(queryParts, "&") } var headers []string if len(r.Headers) > 0 { var headerKeys []string for k := range r.Headers { headerKeys = append(headerKeys, k) } sort.Strings(headerKeys) for _, k := range headerKeys { for _, val := range r.Headers[k] { headers = append(headers, fmt.Sprintf("%s:%s", k, val)) } } } return fmt.Sprintf("%s %s%s\n%s\n%s", r.Method, r.Route, queryString, headers, r.Body) } // Response is a simplified http.Response object type Response struct { // Statuscode is the http status code of the Response StatusCode int // Headers are the http headers of this Response Headers http.Header // Body is the response body Body []byte } // testHandler is an http.Handler with a defined mapping from Request to an // ordered list of Response objects type testHandler struct { responseMap map[string][]Response } // NewHandler returns a new test handler that responds to defined requests // with specified responses // Each time a Request is received, the next Response is returned in the // mapping, until no Responses are defined, at which point a 404 is sent back func NewHandler(requestResponseMap RequestResponseMap) http.Handler { responseMap := make(map[string][]Response) for _, mapping := range requestResponseMap { responses, ok := responseMap[mapping.Request.String()] if ok { responseMap[mapping.Request.String()] = append(responses, mapping.Response) } else { responseMap[mapping.Request.String()] = []Response{mapping.Response} } } return &testHandler{responseMap: responseMap} } func (app *testHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() requestBody, _ := ioutil.ReadAll(r.Body) request := Request{ Method: r.Method, Route: r.URL.Path, QueryParams: r.URL.Query(), Body: requestBody, Headers: make(map[string][]string), } // Add headers of interest here for k, v := range r.Header { if k == "If-None-Match" { request.Headers[k] = v } } responses, ok := app.responseMap[request.String()] if !ok || len(responses) == 0 { http.NotFound(w, r) return } response := responses[0] app.responseMap[request.String()] = responses[1:] responseHeader := w.Header() for k, v := range response.Headers { responseHeader[k] = v } w.WriteHeader(response.StatusCode) io.Copy(w, bytes.NewReader(response.Body)) } docker-registry-2.6.2~ds1/testutil/manifests.go000066400000000000000000000053341313450123100216370ustar00rootroot00000000000000package testutil import ( "fmt" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" "github.com/docker/distribution/manifest" "github.com/docker/distribution/manifest/manifestlist" "github.com/docker/distribution/manifest/schema1" "github.com/docker/distribution/manifest/schema2" "github.com/docker/libtrust" ) // MakeManifestList constructs a manifest list out of a list of manifest digests func MakeManifestList(blobstatter distribution.BlobStatter, manifestDigests []digest.Digest) (*manifestlist.DeserializedManifestList, error) { ctx := context.Background() var manifestDescriptors []manifestlist.ManifestDescriptor for _, manifestDigest := range manifestDigests { descriptor, err := blobstatter.Stat(ctx, manifestDigest) if err != nil { return nil, err } platformSpec := manifestlist.PlatformSpec{ Architecture: "atari2600", OS: "CP/M", Variant: "ternary", Features: []string{"VLIW", "superscalaroutoforderdevnull"}, } manifestDescriptor := manifestlist.ManifestDescriptor{ Descriptor: descriptor, Platform: platformSpec, } manifestDescriptors = append(manifestDescriptors, manifestDescriptor) } return manifestlist.FromDescriptors(manifestDescriptors) } // MakeSchema1Manifest constructs a schema 1 manifest from a given list of digests and returns // the digest of the manifest func MakeSchema1Manifest(digests []digest.Digest) (distribution.Manifest, error) { manifest := schema1.Manifest{ Versioned: manifest.Versioned{ SchemaVersion: 1, }, Name: "who", Tag: "cares", } for _, digest := range digests { manifest.FSLayers = append(manifest.FSLayers, schema1.FSLayer{BlobSum: digest}) manifest.History = append(manifest.History, schema1.History{V1Compatibility: ""}) } pk, err := libtrust.GenerateECP256PrivateKey() if err != nil { return nil, fmt.Errorf("unexpected error generating private key: %v", err) } signedManifest, err := schema1.Sign(&manifest, pk) if err != nil { return nil, fmt.Errorf("error signing manifest: %v", err) } return signedManifest, nil } // MakeSchema2Manifest constructs a schema 2 manifest from a given list of digests and returns // the digest of the manifest func MakeSchema2Manifest(repository distribution.Repository, digests []digest.Digest) (distribution.Manifest, error) { ctx := context.Background() blobStore := repository.Blobs(ctx) builder := schema2.NewManifestBuilder(blobStore, []byte{}) for _, digest := range digests { builder.AppendReference(distribution.Descriptor{Digest: digest}) } manifest, err := builder.Build(ctx) if err != nil { return nil, fmt.Errorf("unexpected error generating manifest: %v", err) } return manifest, nil } docker-registry-2.6.2~ds1/testutil/tarfile.go000066400000000000000000000056311313450123100212740ustar00rootroot00000000000000package testutil import ( "archive/tar" "bytes" "fmt" "io" mrand "math/rand" "time" "github.com/docker/distribution" "github.com/docker/distribution/context" "github.com/docker/distribution/digest" ) // CreateRandomTarFile creates a random tarfile, returning it as an // io.ReadSeeker along with its digest. An error is returned if there is a // problem generating valid content. func CreateRandomTarFile() (rs io.ReadSeeker, dgst digest.Digest, err error) { nFiles := mrand.Intn(10) + 10 target := &bytes.Buffer{} wr := tar.NewWriter(target) // Perturb this on each iteration of the loop below. header := &tar.Header{ Mode: 0644, ModTime: time.Now(), Typeflag: tar.TypeReg, Uname: "randocalrissian", Gname: "cloudcity", AccessTime: time.Now(), ChangeTime: time.Now(), } for fileNumber := 0; fileNumber < nFiles; fileNumber++ { fileSize := mrand.Int63n(1<<20) + 1<<20 header.Name = fmt.Sprint(fileNumber) header.Size = fileSize if err := wr.WriteHeader(header); err != nil { return nil, "", err } randomData := make([]byte, fileSize) // Fill up the buffer with some random data. n, err := mrand.Read(randomData) if n != len(randomData) { return nil, "", fmt.Errorf("short read creating random reader: %v bytes != %v bytes", n, len(randomData)) } if err != nil { return nil, "", err } nn, err := io.Copy(wr, bytes.NewReader(randomData)) if nn != fileSize { return nil, "", fmt.Errorf("short copy writing random file to tar") } if err != nil { return nil, "", err } if err := wr.Flush(); err != nil { return nil, "", err } } if err := wr.Close(); err != nil { return nil, "", err } dgst = digest.FromBytes(target.Bytes()) return bytes.NewReader(target.Bytes()), dgst, nil } // CreateRandomLayers returns a map of n digests. We don't particularly care // about the order of said digests (since they're all random anyway). func CreateRandomLayers(n int) (map[digest.Digest]io.ReadSeeker, error) { digestMap := map[digest.Digest]io.ReadSeeker{} for i := 0; i < n; i++ { rs, ds, err := CreateRandomTarFile() if err != nil { return nil, fmt.Errorf("unexpected error generating test layer file: %v", err) } dgst := digest.Digest(ds) digestMap[dgst] = rs } return digestMap, nil } // UploadBlobs lets you upload blobs to a repository func UploadBlobs(repository distribution.Repository, layers map[digest.Digest]io.ReadSeeker) error { ctx := context.Background() for digest, rs := range layers { wr, err := repository.Blobs(ctx).Create(ctx) if err != nil { return fmt.Errorf("unexpected error creating upload: %v", err) } if _, err := io.Copy(wr, rs); err != nil { return fmt.Errorf("unexpected error copying to upload: %v", err) } if _, err := wr.Commit(ctx, distribution.Descriptor{Digest: digest}); err != nil { return fmt.Errorf("unexpected error committinng upload: %v", err) } } return nil } docker-registry-2.6.2~ds1/uuid/000077500000000000000000000000001313450123100164035ustar00rootroot00000000000000docker-registry-2.6.2~ds1/uuid/uuid.go000066400000000000000000000060331313450123100177020ustar00rootroot00000000000000// Package uuid provides simple UUID generation. Only version 4 style UUIDs // can be generated. // // Please see http://tools.ietf.org/html/rfc4122 for details on UUIDs. package uuid import ( "crypto/rand" "fmt" "io" "os" "syscall" "time" ) const ( // Bits is the number of bits in a UUID Bits = 128 // Size is the number of bytes in a UUID Size = Bits / 8 format = "%08x-%04x-%04x-%04x-%012x" ) var ( // ErrUUIDInvalid indicates a parsed string is not a valid uuid. ErrUUIDInvalid = fmt.Errorf("invalid uuid") // Loggerf can be used to override the default logging destination. Such // log messages in this library should be logged at warning or higher. Loggerf = func(format string, args ...interface{}) {} ) // UUID represents a UUID value. UUIDs can be compared and set to other values // and accessed by byte. type UUID [Size]byte // Generate creates a new, version 4 uuid. func Generate() (u UUID) { const ( // ensures we backoff for less than 450ms total. Use the following to // select new value, in units of 10ms: // n*(n+1)/2 = d -> n^2 + n - 2d -> n = (sqrt(8d + 1) - 1)/2 maxretries = 9 backoff = time.Millisecond * 10 ) var ( totalBackoff time.Duration count int retries int ) for { // This should never block but the read may fail. Because of this, // we just try to read the random number generator until we get // something. This is a very rare condition but may happen. b := time.Duration(retries) * backoff time.Sleep(b) totalBackoff += b n, err := io.ReadFull(rand.Reader, u[count:]) if err != nil { if retryOnError(err) && retries < maxretries { count += n retries++ Loggerf("error generating version 4 uuid, retrying: %v", err) continue } // Any other errors represent a system problem. What did someone // do to /dev/urandom? panic(fmt.Errorf("error reading random number generator, retried for %v: %v", totalBackoff.String(), err)) } break } u[6] = (u[6] & 0x0f) | 0x40 // set version byte u[8] = (u[8] & 0x3f) | 0x80 // set high order byte 0b10{8,9,a,b} return u } // Parse attempts to extract a uuid from the string or returns an error. func Parse(s string) (u UUID, err error) { if len(s) != 36 { return UUID{}, ErrUUIDInvalid } // create stack addresses for each section of the uuid. p := make([][]byte, 5) if _, err := fmt.Sscanf(s, format, &p[0], &p[1], &p[2], &p[3], &p[4]); err != nil { return u, err } copy(u[0:4], p[0]) copy(u[4:6], p[1]) copy(u[6:8], p[2]) copy(u[8:10], p[3]) copy(u[10:16], p[4]) return } func (u UUID) String() string { return fmt.Sprintf(format, u[:4], u[4:6], u[6:8], u[8:10], u[10:]) } // retryOnError tries to detect whether or not retrying would be fruitful. func retryOnError(err error) bool { switch err := err.(type) { case *os.PathError: return retryOnError(err.Err) // unpack the target error case syscall.Errno: if err == syscall.EPERM { // EPERM represents an entropy pool exhaustion, a condition under // which we backoff and retry. return true } } return false } docker-registry-2.6.2~ds1/uuid/uuid_test.go000066400000000000000000000020671313450123100207440ustar00rootroot00000000000000package uuid import ( "testing" ) const iterations = 1000 func TestUUID4Generation(t *testing.T) { for i := 0; i < iterations; i++ { u := Generate() if u[6]&0xf0 != 0x40 { t.Fatalf("version byte not correctly set: %v, %08b %08b", u, u[6], u[6]&0xf0) } if u[8]&0xc0 != 0x80 { t.Fatalf("top order 8th byte not correctly set: %v, %b", u, u[8]) } } } func TestParseAndEquality(t *testing.T) { for i := 0; i < iterations; i++ { u := Generate() parsed, err := Parse(u.String()) if err != nil { t.Fatalf("error parsing uuid %v: %v", u, err) } if parsed != u { t.Fatalf("parsing round trip failed: %v != %v", parsed, u) } } for _, c := range []string{ "bad", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // correct length, incorrect format " 20cc7775-2671-43c7-8742-51d1cfa23258", // leading space "20cc7775-2671-43c7-8742-51d1cfa23258 ", // trailing space "00000000-0000-0000-0000-x00000000000", // out of range character } { if _, err := Parse(c); err == nil { t.Fatalf("parsing %q should have failed", c) } } } docker-registry-2.6.2~ds1/vendor/000077500000000000000000000000001313450123100167325ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/000077500000000000000000000000001313450123100207715ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/Azure/000077500000000000000000000000001313450123100220575ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/000077500000000000000000000000001313450123100251535ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/LICENSE000066400000000000000000000261301313450123100261620ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016 Microsoft Corporation Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/000077500000000000000000000000001313450123100266175ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/README.md000066400000000000000000000011171313450123100300760ustar00rootroot00000000000000# Azure Storage SDK for Go The `github.com/Azure/azure-sdk-for-go/storage` package is used to perform operations in Azure Storage Service. To manage your storage accounts (Azure Resource Manager / ARM), use the [github.com/Azure/azure-sdk-for-go/arm/storage](../arm/storage) package. For your classic storage accounts (Azure Service Management / ASM), use [github.com/Azure/azure-sdk-for-go/management/storageservice](../management/storageservice) package. This package includes support for [Azure Storage Emulator](https://azure.microsoft.com/documentation/articles/storage-use-emulator/)docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/blob.go000066400000000000000000001246401313450123100300730ustar00rootroot00000000000000package storage import ( "bytes" "encoding/xml" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) // BlobStorageClient contains operations for Microsoft Azure Blob Storage // Service. type BlobStorageClient struct { client Client } // A Container is an entry in ContainerListResponse. type Container struct { Name string `xml:"Name"` Properties ContainerProperties `xml:"Properties"` // TODO (ahmetalpbalkan) Metadata } // ContainerProperties contains various properties of a container returned from // various endpoints like ListContainers. type ContainerProperties struct { LastModified string `xml:"Last-Modified"` Etag string `xml:"Etag"` LeaseStatus string `xml:"LeaseStatus"` LeaseState string `xml:"LeaseState"` LeaseDuration string `xml:"LeaseDuration"` // TODO (ahmetalpbalkan) remaining fields } // ContainerListResponse contains the response fields from // ListContainers call. // // See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx type ContainerListResponse struct { XMLName xml.Name `xml:"EnumerationResults"` Xmlns string `xml:"xmlns,attr"` Prefix string `xml:"Prefix"` Marker string `xml:"Marker"` NextMarker string `xml:"NextMarker"` MaxResults int64 `xml:"MaxResults"` Containers []Container `xml:"Containers>Container"` } // A Blob is an entry in BlobListResponse. type Blob struct { Name string `xml:"Name"` Properties BlobProperties `xml:"Properties"` Metadata BlobMetadata `xml:"Metadata"` } // BlobMetadata is a set of custom name/value pairs. // // See https://msdn.microsoft.com/en-us/library/azure/dd179404.aspx type BlobMetadata map[string]string type blobMetadataEntries struct { Entries []blobMetadataEntry `xml:",any"` } type blobMetadataEntry struct { XMLName xml.Name Value string `xml:",chardata"` } // UnmarshalXML converts the xml:Metadata into Metadata map func (bm *BlobMetadata) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var entries blobMetadataEntries if err := d.DecodeElement(&entries, &start); err != nil { return err } for _, entry := range entries.Entries { if *bm == nil { *bm = make(BlobMetadata) } (*bm)[strings.ToLower(entry.XMLName.Local)] = entry.Value } return nil } // MarshalXML implements the xml.Marshaler interface. It encodes // metadata name/value pairs as they would appear in an Azure // ListBlobs response. func (bm BlobMetadata) MarshalXML(enc *xml.Encoder, start xml.StartElement) error { entries := make([]blobMetadataEntry, 0, len(bm)) for k, v := range bm { entries = append(entries, blobMetadataEntry{ XMLName: xml.Name{Local: http.CanonicalHeaderKey(k)}, Value: v, }) } return enc.EncodeElement(blobMetadataEntries{ Entries: entries, }, start) } // BlobProperties contains various properties of a blob // returned in various endpoints like ListBlobs or GetBlobProperties. type BlobProperties struct { LastModified string `xml:"Last-Modified"` Etag string `xml:"Etag"` ContentMD5 string `xml:"Content-MD5"` ContentLength int64 `xml:"Content-Length"` ContentType string `xml:"Content-Type"` ContentEncoding string `xml:"Content-Encoding"` CacheControl string `xml:"Cache-Control"` ContentLanguage string `xml:"Cache-Language"` BlobType BlobType `xml:"x-ms-blob-blob-type"` SequenceNumber int64 `xml:"x-ms-blob-sequence-number"` CopyID string `xml:"CopyId"` CopyStatus string `xml:"CopyStatus"` CopySource string `xml:"CopySource"` CopyProgress string `xml:"CopyProgress"` CopyCompletionTime string `xml:"CopyCompletionTime"` CopyStatusDescription string `xml:"CopyStatusDescription"` LeaseStatus string `xml:"LeaseStatus"` } // BlobHeaders contains various properties of a blob and is an entry // in SetBlobProperties type BlobHeaders struct { ContentMD5 string `header:"x-ms-blob-content-md5"` ContentLanguage string `header:"x-ms-blob-content-language"` ContentEncoding string `header:"x-ms-blob-content-encoding"` ContentType string `header:"x-ms-blob-content-type"` CacheControl string `header:"x-ms-blob-cache-control"` } // BlobListResponse contains the response fields from ListBlobs call. // // See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx type BlobListResponse struct { XMLName xml.Name `xml:"EnumerationResults"` Xmlns string `xml:"xmlns,attr"` Prefix string `xml:"Prefix"` Marker string `xml:"Marker"` NextMarker string `xml:"NextMarker"` MaxResults int64 `xml:"MaxResults"` Blobs []Blob `xml:"Blobs>Blob"` // BlobPrefix is used to traverse blobs as if it were a file system. // It is returned if ListBlobsParameters.Delimiter is specified. // The list here can be thought of as "folders" that may contain // other folders or blobs. BlobPrefixes []string `xml:"Blobs>BlobPrefix>Name"` // Delimiter is used to traverse blobs as if it were a file system. // It is returned if ListBlobsParameters.Delimiter is specified. Delimiter string `xml:"Delimiter"` } // ListContainersParameters defines the set of customizable parameters to make a // List Containers call. // // See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx type ListContainersParameters struct { Prefix string Marker string Include string MaxResults uint Timeout uint } func (p ListContainersParameters) getParameters() url.Values { out := url.Values{} if p.Prefix != "" { out.Set("prefix", p.Prefix) } if p.Marker != "" { out.Set("marker", p.Marker) } if p.Include != "" { out.Set("include", p.Include) } if p.MaxResults != 0 { out.Set("maxresults", fmt.Sprintf("%v", p.MaxResults)) } if p.Timeout != 0 { out.Set("timeout", fmt.Sprintf("%v", p.Timeout)) } return out } // ListBlobsParameters defines the set of customizable // parameters to make a List Blobs call. // // See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx type ListBlobsParameters struct { Prefix string Delimiter string Marker string Include string MaxResults uint Timeout uint } func (p ListBlobsParameters) getParameters() url.Values { out := url.Values{} if p.Prefix != "" { out.Set("prefix", p.Prefix) } if p.Delimiter != "" { out.Set("delimiter", p.Delimiter) } if p.Marker != "" { out.Set("marker", p.Marker) } if p.Include != "" { out.Set("include", p.Include) } if p.MaxResults != 0 { out.Set("maxresults", fmt.Sprintf("%v", p.MaxResults)) } if p.Timeout != 0 { out.Set("timeout", fmt.Sprintf("%v", p.Timeout)) } return out } // BlobType defines the type of the Azure Blob. type BlobType string // Types of page blobs const ( BlobTypeBlock BlobType = "BlockBlob" BlobTypePage BlobType = "PageBlob" BlobTypeAppend BlobType = "AppendBlob" ) // PageWriteType defines the type updates that are going to be // done on the page blob. type PageWriteType string // Types of operations on page blobs const ( PageWriteTypeUpdate PageWriteType = "update" PageWriteTypeClear PageWriteType = "clear" ) const ( blobCopyStatusPending = "pending" blobCopyStatusSuccess = "success" blobCopyStatusAborted = "aborted" blobCopyStatusFailed = "failed" ) // lease constants. const ( leaseHeaderPrefix = "x-ms-lease-" leaseID = "x-ms-lease-id" leaseAction = "x-ms-lease-action" leaseBreakPeriod = "x-ms-lease-break-period" leaseDuration = "x-ms-lease-duration" leaseProposedID = "x-ms-proposed-lease-id" leaseTime = "x-ms-lease-time" acquireLease = "acquire" renewLease = "renew" changeLease = "change" releaseLease = "release" breakLease = "break" ) // BlockListType is used to filter out types of blocks in a Get Blocks List call // for a block blob. // // See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx for all // block types. type BlockListType string // Filters for listing blocks in block blobs const ( BlockListTypeAll BlockListType = "all" BlockListTypeCommitted BlockListType = "committed" BlockListTypeUncommitted BlockListType = "uncommitted" ) // ContainerAccessType defines the access level to the container from a public // request. // // See https://msdn.microsoft.com/en-us/library/azure/dd179468.aspx and "x-ms- // blob-public-access" header. type ContainerAccessType string // Access options for containers const ( ContainerAccessTypePrivate ContainerAccessType = "" ContainerAccessTypeBlob ContainerAccessType = "blob" ContainerAccessTypeContainer ContainerAccessType = "container" ) // Maximum sizes (per REST API) for various concepts const ( MaxBlobBlockSize = 4 * 1024 * 1024 MaxBlobPageSize = 4 * 1024 * 1024 ) // BlockStatus defines states a block for a block blob can // be in. type BlockStatus string // List of statuses that can be used to refer to a block in a block list const ( BlockStatusUncommitted BlockStatus = "Uncommitted" BlockStatusCommitted BlockStatus = "Committed" BlockStatusLatest BlockStatus = "Latest" ) // Block is used to create Block entities for Put Block List // call. type Block struct { ID string Status BlockStatus } // BlockListResponse contains the response fields from Get Block List call. // // See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx type BlockListResponse struct { XMLName xml.Name `xml:"BlockList"` CommittedBlocks []BlockResponse `xml:"CommittedBlocks>Block"` UncommittedBlocks []BlockResponse `xml:"UncommittedBlocks>Block"` } // BlockResponse contains the block information returned // in the GetBlockListCall. type BlockResponse struct { Name string `xml:"Name"` Size int64 `xml:"Size"` } // GetPageRangesResponse contains the reponse fields from // Get Page Ranges call. // // See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx type GetPageRangesResponse struct { XMLName xml.Name `xml:"PageList"` PageList []PageRange `xml:"PageRange"` } // PageRange contains information about a page of a page blob from // Get Pages Range call. // // See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx type PageRange struct { Start int64 `xml:"Start"` End int64 `xml:"End"` } var ( errBlobCopyAborted = errors.New("storage: blob copy is aborted") errBlobCopyIDMismatch = errors.New("storage: blob copy id is a mismatch") ) // ListContainers returns the list of containers in a storage account along with // pagination token and other response details. // // See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx func (b BlobStorageClient) ListContainers(params ListContainersParameters) (ContainerListResponse, error) { q := mergeParams(params.getParameters(), url.Values{"comp": {"list"}}) uri := b.client.getEndpoint(blobServiceName, "", q) headers := b.client.getStandardHeaders() var out ContainerListResponse resp, err := b.client.exec("GET", uri, headers, nil) if err != nil { return out, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &out) return out, err } // CreateContainer creates a blob container within the storage account // with given name and access level. Returns error if container already exists. // // See https://msdn.microsoft.com/en-us/library/azure/dd179468.aspx func (b BlobStorageClient) CreateContainer(name string, access ContainerAccessType) error { resp, err := b.createContainer(name, access) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // CreateContainerIfNotExists creates a blob container if it does not exist. Returns // true if container is newly created or false if container already exists. func (b BlobStorageClient) CreateContainerIfNotExists(name string, access ContainerAccessType) (bool, error) { resp, err := b.createContainer(name, access) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict { return resp.statusCode == http.StatusCreated, nil } } return false, err } func (b BlobStorageClient) createContainer(name string, access ContainerAccessType) (*storageResponse, error) { verb := "PUT" uri := b.client.getEndpoint(blobServiceName, pathForContainer(name), url.Values{"restype": {"container"}}) headers := b.client.getStandardHeaders() if access != "" { headers["x-ms-blob-public-access"] = string(access) } return b.client.exec(verb, uri, headers, nil) } // ContainerExists returns true if a container with given name exists // on the storage account, otherwise returns false. func (b BlobStorageClient) ContainerExists(name string) (bool, error) { verb := "HEAD" uri := b.client.getEndpoint(blobServiceName, pathForContainer(name), url.Values{"restype": {"container"}}) headers := b.client.getStandardHeaders() resp, err := b.client.exec(verb, uri, headers, nil) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusOK, nil } } return false, err } // DeleteContainer deletes the container with given name on the storage // account. If the container does not exist returns error. // // See https://msdn.microsoft.com/en-us/library/azure/dd179408.aspx func (b BlobStorageClient) DeleteContainer(name string) error { resp, err := b.deleteContainer(name) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusAccepted}) } // DeleteContainerIfExists deletes the container with given name on the storage // account if it exists. Returns true if container is deleted with this call, or // false if the container did not exist at the time of the Delete Container // operation. // // See https://msdn.microsoft.com/en-us/library/azure/dd179408.aspx func (b BlobStorageClient) DeleteContainerIfExists(name string) (bool, error) { resp, err := b.deleteContainer(name) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusAccepted, nil } } return false, err } func (b BlobStorageClient) deleteContainer(name string) (*storageResponse, error) { verb := "DELETE" uri := b.client.getEndpoint(blobServiceName, pathForContainer(name), url.Values{"restype": {"container"}}) headers := b.client.getStandardHeaders() return b.client.exec(verb, uri, headers, nil) } // ListBlobs returns an object that contains list of blobs in the container, // pagination token and other information in the response of List Blobs call. // // See https://msdn.microsoft.com/en-us/library/azure/dd135734.aspx func (b BlobStorageClient) ListBlobs(container string, params ListBlobsParameters) (BlobListResponse, error) { q := mergeParams(params.getParameters(), url.Values{ "restype": {"container"}, "comp": {"list"}}) uri := b.client.getEndpoint(blobServiceName, pathForContainer(container), q) headers := b.client.getStandardHeaders() var out BlobListResponse resp, err := b.client.exec("GET", uri, headers, nil) if err != nil { return out, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &out) return out, err } // BlobExists returns true if a blob with given name exists on the specified // container of the storage account. func (b BlobStorageClient) BlobExists(container, name string) (bool, error) { verb := "HEAD" uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) headers := b.client.getStandardHeaders() resp, err := b.client.exec(verb, uri, headers, nil) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusOK, nil } } return false, err } // GetBlobURL gets the canonical URL to the blob with the specified name in the // specified container. This method does not create a publicly accessible URL if // the blob or container is private and this method does not check if the blob // exists. func (b BlobStorageClient) GetBlobURL(container, name string) string { if container == "" { container = "$root" } return b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) } // GetBlob returns a stream to read the blob. Caller must call Close() the // reader to close on the underlying connection. // // See https://msdn.microsoft.com/en-us/library/azure/dd179440.aspx func (b BlobStorageClient) GetBlob(container, name string) (io.ReadCloser, error) { resp, err := b.getBlobRange(container, name, "", nil) if err != nil { return nil, err } if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } return resp.body, nil } // GetBlobRange reads the specified range of a blob to a stream. The bytesRange // string must be in a format like "0-", "10-100" as defined in HTTP 1.1 spec. // // See https://msdn.microsoft.com/en-us/library/azure/dd179440.aspx func (b BlobStorageClient) GetBlobRange(container, name, bytesRange string, extraHeaders map[string]string) (io.ReadCloser, error) { resp, err := b.getBlobRange(container, name, bytesRange, extraHeaders) if err != nil { return nil, err } if err := checkRespCode(resp.statusCode, []int{http.StatusPartialContent}); err != nil { return nil, err } return resp.body, nil } func (b BlobStorageClient) getBlobRange(container, name, bytesRange string, extraHeaders map[string]string) (*storageResponse, error) { verb := "GET" uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) headers := b.client.getStandardHeaders() if bytesRange != "" { headers["Range"] = fmt.Sprintf("bytes=%s", bytesRange) } for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec(verb, uri, headers, nil) if err != nil { return nil, err } return resp, err } // leasePut is common PUT code for the various aquire/release/break etc functions. func (b BlobStorageClient) leaseCommonPut(container string, name string, headers map[string]string, expectedStatus int) (http.Header, error) { params := url.Values{"comp": {"lease"}} uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params) resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{expectedStatus}); err != nil { return nil, err } return resp.headers, nil } // AcquireLease creates a lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx // returns leaseID acquired func (b BlobStorageClient) AcquireLease(container string, name string, leaseTimeInSeconds int, proposedLeaseID string) (returnedLeaseID string, err error) { headers := b.client.getStandardHeaders() headers[leaseAction] = acquireLease headers[leaseProposedID] = proposedLeaseID headers[leaseDuration] = strconv.Itoa(leaseTimeInSeconds) respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusCreated) if err != nil { return "", err } returnedLeaseID = respHeaders.Get(http.CanonicalHeaderKey(leaseID)) if returnedLeaseID != "" { return returnedLeaseID, nil } // what should we return in case of HTTP 201 but no lease ID? // or it just cant happen? (brave words) return "", errors.New("LeaseID not returned") } // BreakLease breaks the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx // Returns the timeout remaining in the lease in seconds func (b BlobStorageClient) BreakLease(container string, name string) (breakTimeout int, err error) { headers := b.client.getStandardHeaders() headers[leaseAction] = breakLease return b.breakLeaseCommon(container, name, headers) } // BreakLeaseWithBreakPeriod breaks the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx // breakPeriodInSeconds is used to determine how long until new lease can be created. // Returns the timeout remaining in the lease in seconds func (b BlobStorageClient) BreakLeaseWithBreakPeriod(container string, name string, breakPeriodInSeconds int) (breakTimeout int, err error) { headers := b.client.getStandardHeaders() headers[leaseAction] = breakLease headers[leaseBreakPeriod] = strconv.Itoa(breakPeriodInSeconds) return b.breakLeaseCommon(container, name, headers) } // breakLeaseCommon is common code for both version of BreakLease (with and without break period) func (b BlobStorageClient) breakLeaseCommon(container string, name string, headers map[string]string) (breakTimeout int, err error) { respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusAccepted) if err != nil { return 0, err } breakTimeoutStr := respHeaders.Get(http.CanonicalHeaderKey(leaseTime)) if breakTimeoutStr != "" { breakTimeout, err = strconv.Atoi(breakTimeoutStr) if err != nil { return 0, err } } return breakTimeout, nil } // ChangeLease changes a lease ID for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx // Returns the new LeaseID acquired func (b BlobStorageClient) ChangeLease(container string, name string, currentLeaseID string, proposedLeaseID string) (newLeaseID string, err error) { headers := b.client.getStandardHeaders() headers[leaseAction] = changeLease headers[leaseID] = currentLeaseID headers[leaseProposedID] = proposedLeaseID respHeaders, err := b.leaseCommonPut(container, name, headers, http.StatusOK) if err != nil { return "", err } newLeaseID = respHeaders.Get(http.CanonicalHeaderKey(leaseID)) if newLeaseID != "" { return newLeaseID, nil } return "", errors.New("LeaseID not returned") } // ReleaseLease releases the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx func (b BlobStorageClient) ReleaseLease(container string, name string, currentLeaseID string) error { headers := b.client.getStandardHeaders() headers[leaseAction] = releaseLease headers[leaseID] = currentLeaseID _, err := b.leaseCommonPut(container, name, headers, http.StatusOK) if err != nil { return err } return nil } // RenewLease renews the lease for a blob as per https://msdn.microsoft.com/en-us/library/azure/ee691972.aspx func (b BlobStorageClient) RenewLease(container string, name string, currentLeaseID string) error { headers := b.client.getStandardHeaders() headers[leaseAction] = renewLease headers[leaseID] = currentLeaseID _, err := b.leaseCommonPut(container, name, headers, http.StatusOK) if err != nil { return err } return nil } // GetBlobProperties provides various information about the specified // blob. See https://msdn.microsoft.com/en-us/library/azure/dd179394.aspx func (b BlobStorageClient) GetBlobProperties(container, name string) (*BlobProperties, error) { verb := "HEAD" uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) headers := b.client.getStandardHeaders() resp, err := b.client.exec(verb, uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } var contentLength int64 contentLengthStr := resp.headers.Get("Content-Length") if contentLengthStr != "" { contentLength, err = strconv.ParseInt(contentLengthStr, 0, 64) if err != nil { return nil, err } } var sequenceNum int64 sequenceNumStr := resp.headers.Get("x-ms-blob-sequence-number") if sequenceNumStr != "" { sequenceNum, err = strconv.ParseInt(sequenceNumStr, 0, 64) if err != nil { return nil, err } } return &BlobProperties{ LastModified: resp.headers.Get("Last-Modified"), Etag: resp.headers.Get("Etag"), ContentMD5: resp.headers.Get("Content-MD5"), ContentLength: contentLength, ContentEncoding: resp.headers.Get("Content-Encoding"), ContentType: resp.headers.Get("Content-Type"), CacheControl: resp.headers.Get("Cache-Control"), ContentLanguage: resp.headers.Get("Content-Language"), SequenceNumber: sequenceNum, CopyCompletionTime: resp.headers.Get("x-ms-copy-completion-time"), CopyStatusDescription: resp.headers.Get("x-ms-copy-status-description"), CopyID: resp.headers.Get("x-ms-copy-id"), CopyProgress: resp.headers.Get("x-ms-copy-progress"), CopySource: resp.headers.Get("x-ms-copy-source"), CopyStatus: resp.headers.Get("x-ms-copy-status"), BlobType: BlobType(resp.headers.Get("x-ms-blob-type")), LeaseStatus: resp.headers.Get("x-ms-lease-status"), }, nil } // SetBlobProperties replaces the BlobHeaders for the specified blob. // // Some keys may be converted to Camel-Case before sending. All keys // are returned in lower case by GetBlobProperties. HTTP header names // are case-insensitive so case munging should not matter to other // applications either. // // See https://msdn.microsoft.com/en-us/library/azure/ee691966.aspx func (b BlobStorageClient) SetBlobProperties(container, name string, blobHeaders BlobHeaders) error { params := url.Values{"comp": {"properties"}} uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params) headers := b.client.getStandardHeaders() extraHeaders := headersFromStruct(blobHeaders) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusOK}) } // SetBlobMetadata replaces the metadata for the specified blob. // // Some keys may be converted to Camel-Case before sending. All keys // are returned in lower case by GetBlobMetadata. HTTP header names // are case-insensitive so case munging should not matter to other // applications either. // // See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx func (b BlobStorageClient) SetBlobMetadata(container, name string, metadata map[string]string, extraHeaders map[string]string) error { params := url.Values{"comp": {"metadata"}} uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params) headers := b.client.getStandardHeaders() for k, v := range metadata { headers[userDefinedMetadataHeaderPrefix+k] = v } for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusOK}) } // GetBlobMetadata returns all user-defined metadata for the specified blob. // // All metadata keys will be returned in lower case. (HTTP header // names are case-insensitive.) // // See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx func (b BlobStorageClient) GetBlobMetadata(container, name string) (map[string]string, error) { params := url.Values{"comp": {"metadata"}} uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params) headers := b.client.getStandardHeaders() resp, err := b.client.exec("GET", uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } metadata := make(map[string]string) for k, v := range resp.headers { // Can't trust CanonicalHeaderKey() to munge case // reliably. "_" is allowed in identifiers: // https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx // https://msdn.microsoft.com/library/aa664670(VS.71).aspx // http://tools.ietf.org/html/rfc7230#section-3.2 // ...but "_" is considered invalid by // CanonicalMIMEHeaderKey in // https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542 // so k can be "X-Ms-Meta-Foo" or "x-ms-meta-foo_bar". k = strings.ToLower(k) if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) { continue } // metadata["foo"] = content of the last X-Ms-Meta-Foo header k = k[len(userDefinedMetadataHeaderPrefix):] metadata[k] = v[len(v)-1] } return metadata, nil } // CreateBlockBlob initializes an empty block blob with no blocks. // // See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx func (b BlobStorageClient) CreateBlockBlob(container, name string) error { return b.CreateBlockBlobFromReader(container, name, 0, nil, nil) } // CreateBlockBlobFromReader initializes a block blob using data from // reader. Size must be the number of bytes read from reader. To // create an empty blob, use size==0 and reader==nil. // // The API rejects requests with size > 64 MiB (but this limit is not // checked by the SDK). To write a larger blob, use CreateBlockBlob, // PutBlock, and PutBlockList. // // See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx func (b BlobStorageClient) CreateBlockBlobFromReader(container, name string, size uint64, blob io.Reader, extraHeaders map[string]string) error { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypeBlock) headers["Content-Length"] = fmt.Sprintf("%d", size) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, blob) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // PutBlock saves the given data chunk to the specified block blob with // given ID. // // The API rejects chunks larger than 4 MiB (but this limit is not // checked by the SDK). // // See https://msdn.microsoft.com/en-us/library/azure/dd135726.aspx func (b BlobStorageClient) PutBlock(container, name, blockID string, chunk []byte) error { return b.PutBlockWithLength(container, name, blockID, uint64(len(chunk)), bytes.NewReader(chunk), nil) } // PutBlockWithLength saves the given data stream of exactly specified size to // the block blob with given ID. It is an alternative to PutBlocks where data // comes as stream but the length is known in advance. // // The API rejects requests with size > 4 MiB (but this limit is not // checked by the SDK). // // See https://msdn.microsoft.com/en-us/library/azure/dd135726.aspx func (b BlobStorageClient) PutBlockWithLength(container, name, blockID string, size uint64, blob io.Reader, extraHeaders map[string]string) error { uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{"comp": {"block"}, "blockid": {blockID}}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypeBlock) headers["Content-Length"] = fmt.Sprintf("%v", size) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, blob) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // PutBlockList saves list of blocks to the specified block blob. // // See https://msdn.microsoft.com/en-us/library/azure/dd179467.aspx func (b BlobStorageClient) PutBlockList(container, name string, blocks []Block) error { blockListXML := prepareBlockListRequest(blocks) uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{"comp": {"blocklist"}}) headers := b.client.getStandardHeaders() headers["Content-Length"] = fmt.Sprintf("%v", len(blockListXML)) resp, err := b.client.exec("PUT", uri, headers, strings.NewReader(blockListXML)) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // GetBlockList retrieves list of blocks in the specified block blob. // // See https://msdn.microsoft.com/en-us/library/azure/dd179400.aspx func (b BlobStorageClient) GetBlockList(container, name string, blockType BlockListType) (BlockListResponse, error) { params := url.Values{"comp": {"blocklist"}, "blocklisttype": {string(blockType)}} uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), params) headers := b.client.getStandardHeaders() var out BlockListResponse resp, err := b.client.exec("GET", uri, headers, nil) if err != nil { return out, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &out) return out, err } // PutPageBlob initializes an empty page blob with specified name and maximum // size in bytes (size must be aligned to a 512-byte boundary). A page blob must // be created using this method before writing pages. // // See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx func (b BlobStorageClient) PutPageBlob(container, name string, size int64, extraHeaders map[string]string) error { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypePage) headers["x-ms-blob-content-length"] = fmt.Sprintf("%v", size) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // PutPage writes a range of pages to a page blob or clears the given range. // In case of 'clear' writes, given chunk is discarded. Ranges must be aligned // with 512-byte boundaries and chunk must be of size multiplies by 512. // // See https://msdn.microsoft.com/en-us/library/ee691975.aspx func (b BlobStorageClient) PutPage(container, name string, startByte, endByte int64, writeType PageWriteType, chunk []byte, extraHeaders map[string]string) error { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"page"}}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypePage) headers["x-ms-page-write"] = string(writeType) headers["x-ms-range"] = fmt.Sprintf("bytes=%v-%v", startByte, endByte) for k, v := range extraHeaders { headers[k] = v } var contentLength int64 var data io.Reader if writeType == PageWriteTypeClear { contentLength = 0 data = bytes.NewReader([]byte{}) } else { contentLength = int64(len(chunk)) data = bytes.NewReader(chunk) } headers["Content-Length"] = fmt.Sprintf("%v", contentLength) resp, err := b.client.exec("PUT", uri, headers, data) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // GetPageRanges returns the list of valid page ranges for a page blob. // // See https://msdn.microsoft.com/en-us/library/azure/ee691973.aspx func (b BlobStorageClient) GetPageRanges(container, name string) (GetPageRangesResponse, error) { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"pagelist"}}) headers := b.client.getStandardHeaders() var out GetPageRangesResponse resp, err := b.client.exec("GET", uri, headers, nil) if err != nil { return out, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return out, err } err = xmlUnmarshal(resp.body, &out) return out, err } // PutAppendBlob initializes an empty append blob with specified name. An // append blob must be created using this method before appending blocks. // // See https://msdn.microsoft.com/en-us/library/azure/dd179451.aspx func (b BlobStorageClient) PutAppendBlob(container, name string, extraHeaders map[string]string) error { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypeAppend) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // AppendBlock appends a block to an append blob. // // See https://msdn.microsoft.com/en-us/library/azure/mt427365.aspx func (b BlobStorageClient) AppendBlock(container, name string, chunk []byte, extraHeaders map[string]string) error { path := fmt.Sprintf("%s/%s", container, name) uri := b.client.getEndpoint(blobServiceName, path, url.Values{"comp": {"appendblock"}}) headers := b.client.getStandardHeaders() headers["x-ms-blob-type"] = string(BlobTypeAppend) headers["Content-Length"] = fmt.Sprintf("%v", len(chunk)) for k, v := range extraHeaders { headers[k] = v } resp, err := b.client.exec("PUT", uri, headers, bytes.NewReader(chunk)) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // CopyBlob starts a blob copy operation and waits for the operation to // complete. sourceBlob parameter must be a canonical URL to the blob (can be // obtained using GetBlobURL method.) There is no SLA on blob copy and therefore // this helper method works faster on smaller files. // // See https://msdn.microsoft.com/en-us/library/azure/dd894037.aspx func (b BlobStorageClient) CopyBlob(container, name, sourceBlob string) error { copyID, err := b.startBlobCopy(container, name, sourceBlob) if err != nil { return err } return b.waitForBlobCopy(container, name, copyID) } func (b BlobStorageClient) startBlobCopy(container, name, sourceBlob string) (string, error) { uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) headers := b.client.getStandardHeaders() headers["x-ms-copy-source"] = sourceBlob resp, err := b.client.exec("PUT", uri, headers, nil) if err != nil { return "", err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusAccepted, http.StatusCreated}); err != nil { return "", err } copyID := resp.headers.Get("x-ms-copy-id") if copyID == "" { return "", errors.New("Got empty copy id header") } return copyID, nil } func (b BlobStorageClient) waitForBlobCopy(container, name, copyID string) error { for { props, err := b.GetBlobProperties(container, name) if err != nil { return err } if props.CopyID != copyID { return errBlobCopyIDMismatch } switch props.CopyStatus { case blobCopyStatusSuccess: return nil case blobCopyStatusPending: continue case blobCopyStatusAborted: return errBlobCopyAborted case blobCopyStatusFailed: return fmt.Errorf("storage: blob copy failed. Id=%s Description=%s", props.CopyID, props.CopyStatusDescription) default: return fmt.Errorf("storage: unhandled blob copy status: '%s'", props.CopyStatus) } } } // DeleteBlob deletes the given blob from the specified container. // If the blob does not exists at the time of the Delete Blob operation, it // returns error. See https://msdn.microsoft.com/en-us/library/azure/dd179413.aspx func (b BlobStorageClient) DeleteBlob(container, name string, extraHeaders map[string]string) error { resp, err := b.deleteBlob(container, name, extraHeaders) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusAccepted}) } // DeleteBlobIfExists deletes the given blob from the specified container If the // blob is deleted with this call, returns true. Otherwise returns false. // // See https://msdn.microsoft.com/en-us/library/azure/dd179413.aspx func (b BlobStorageClient) DeleteBlobIfExists(container, name string, extraHeaders map[string]string) (bool, error) { resp, err := b.deleteBlob(container, name, extraHeaders) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusAccepted, nil } } return false, err } func (b BlobStorageClient) deleteBlob(container, name string, extraHeaders map[string]string) (*storageResponse, error) { verb := "DELETE" uri := b.client.getEndpoint(blobServiceName, pathForBlob(container, name), url.Values{}) headers := b.client.getStandardHeaders() for k, v := range extraHeaders { headers[k] = v } return b.client.exec(verb, uri, headers, nil) } // helper method to construct the path to a container given its name func pathForContainer(name string) string { return fmt.Sprintf("/%s", name) } // helper method to construct the path to a blob given its container and blob // name func pathForBlob(container, name string) string { return fmt.Sprintf("/%s/%s", container, name) } // GetBlobSASURI creates an URL to the specified blob which contains the Shared // Access Signature with specified permissions and expiration time. // // See https://msdn.microsoft.com/en-us/library/azure/ee395415.aspx func (b BlobStorageClient) GetBlobSASURI(container, name string, expiry time.Time, permissions string) (string, error) { var ( signedPermissions = permissions blobURL = b.GetBlobURL(container, name) ) canonicalizedResource, err := b.client.buildCanonicalizedResource(blobURL) if err != nil { return "", err } // "The canonicalizedresouce portion of the string is a canonical path to the signed resource. // It must include the service name (blob, table, queue or file) for version 2015-02-21 or // later, the storage account name, and the resource name, and must be URL-decoded. // -- https://msdn.microsoft.com/en-us/library/azure/dn140255.aspx // We need to replace + with %2b first to avoid being treated as a space (which is correct for query strings, but not the path component). canonicalizedResource = strings.Replace(canonicalizedResource, "+", "%2b", -1) canonicalizedResource, err = url.QueryUnescape(canonicalizedResource) if err != nil { return "", err } signedExpiry := expiry.UTC().Format(time.RFC3339) signedResource := "b" stringToSign, err := blobSASStringToSign(b.client.apiVersion, canonicalizedResource, signedExpiry, signedPermissions) if err != nil { return "", err } sig := b.client.computeHmac256(stringToSign) sasParams := url.Values{ "sv": {b.client.apiVersion}, "se": {signedExpiry}, "sr": {signedResource}, "sp": {signedPermissions}, "sig": {sig}, } sasURL, err := url.Parse(blobURL) if err != nil { return "", err } sasURL.RawQuery = sasParams.Encode() return sasURL.String(), nil } func blobSASStringToSign(signedVersion, canonicalizedResource, signedExpiry, signedPermissions string) (string, error) { var signedStart, signedIdentifier, rscc, rscd, rsce, rscl, rsct string if signedVersion >= "2015-02-21" { canonicalizedResource = "/blob" + canonicalizedResource } // reference: http://msdn.microsoft.com/en-us/library/azure/dn140255.aspx if signedVersion >= "2013-08-15" { return fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", signedPermissions, signedStart, signedExpiry, canonicalizedResource, signedIdentifier, signedVersion, rscc, rscd, rsce, rscl, rsct), nil } return "", errors.New("storage: not implemented SAS for versions earlier than 2013-08-15") } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/client.go000066400000000000000000000356421313450123100304360ustar00rootroot00000000000000// Package storage provides clients for Microsoft Azure Storage Services. package storage import ( "bytes" "encoding/base64" "encoding/json" "encoding/xml" "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" "regexp" "sort" "strconv" "strings" ) const ( // DefaultBaseURL is the domain name used for storage requests when a // default client is created. DefaultBaseURL = "core.windows.net" // DefaultAPIVersion is the Azure Storage API version string used when a // basic client is created. DefaultAPIVersion = "2015-02-21" defaultUseHTTPS = true // StorageEmulatorAccountName is the fixed storage account used by Azure Storage Emulator StorageEmulatorAccountName = "devstoreaccount1" // StorageEmulatorAccountKey is the the fixed storage account used by Azure Storage Emulator StorageEmulatorAccountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==" blobServiceName = "blob" tableServiceName = "table" queueServiceName = "queue" fileServiceName = "file" storageEmulatorBlob = "127.0.0.1:10000" storageEmulatorTable = "127.0.0.1:10002" storageEmulatorQueue = "127.0.0.1:10001" ) // Client is the object that needs to be constructed to perform // operations on the storage account. type Client struct { // HTTPClient is the http.Client used to initiate API // requests. If it is nil, http.DefaultClient is used. HTTPClient *http.Client accountName string accountKey []byte useHTTPS bool baseURL string apiVersion string } type storageResponse struct { statusCode int headers http.Header body io.ReadCloser } type odataResponse struct { storageResponse odata odataErrorMessage } // AzureStorageServiceError contains fields of the error response from // Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx // Some fields might be specific to certain calls. type AzureStorageServiceError struct { Code string `xml:"Code"` Message string `xml:"Message"` AuthenticationErrorDetail string `xml:"AuthenticationErrorDetail"` QueryParameterName string `xml:"QueryParameterName"` QueryParameterValue string `xml:"QueryParameterValue"` Reason string `xml:"Reason"` StatusCode int RequestID string } type odataErrorMessageMessage struct { Lang string `json:"lang"` Value string `json:"value"` } type odataErrorMessageInternal struct { Code string `json:"code"` Message odataErrorMessageMessage `json:"message"` } type odataErrorMessage struct { Err odataErrorMessageInternal `json:"odata.error"` } // UnexpectedStatusCodeError is returned when a storage service responds with neither an error // nor with an HTTP status code indicating success. type UnexpectedStatusCodeError struct { allowed []int got int } func (e UnexpectedStatusCodeError) Error() string { s := func(i int) string { return fmt.Sprintf("%d %s", i, http.StatusText(i)) } got := s(e.got) expected := []string{} for _, v := range e.allowed { expected = append(expected, s(v)) } return fmt.Sprintf("storage: status code from service response is %s; was expecting %s", got, strings.Join(expected, " or ")) } // Got is the actual status code returned by Azure. func (e UnexpectedStatusCodeError) Got() int { return e.got } // NewBasicClient constructs a Client with given storage service name and // key. func NewBasicClient(accountName, accountKey string) (Client, error) { if accountName == StorageEmulatorAccountName { return NewEmulatorClient() } return NewClient(accountName, accountKey, DefaultBaseURL, DefaultAPIVersion, defaultUseHTTPS) } //NewEmulatorClient contructs a Client intended to only work with Azure //Storage Emulator func NewEmulatorClient() (Client, error) { return NewClient(StorageEmulatorAccountName, StorageEmulatorAccountKey, DefaultBaseURL, DefaultAPIVersion, false) } // NewClient constructs a Client. This should be used if the caller wants // to specify whether to use HTTPS, a specific REST API version or a custom // storage endpoint than Azure Public Cloud. func NewClient(accountName, accountKey, blobServiceBaseURL, apiVersion string, useHTTPS bool) (Client, error) { var c Client if accountName == "" { return c, fmt.Errorf("azure: account name required") } else if accountKey == "" { return c, fmt.Errorf("azure: account key required") } else if blobServiceBaseURL == "" { return c, fmt.Errorf("azure: base storage service url required") } key, err := base64.StdEncoding.DecodeString(accountKey) if err != nil { return c, fmt.Errorf("azure: malformed storage account key: %v", err) } return Client{ accountName: accountName, accountKey: key, useHTTPS: useHTTPS, baseURL: blobServiceBaseURL, apiVersion: apiVersion, }, nil } func (c Client) getBaseURL(service string) string { scheme := "http" if c.useHTTPS { scheme = "https" } host := "" if c.accountName == StorageEmulatorAccountName { switch service { case blobServiceName: host = storageEmulatorBlob case tableServiceName: host = storageEmulatorTable case queueServiceName: host = storageEmulatorQueue } } else { host = fmt.Sprintf("%s.%s.%s", c.accountName, service, c.baseURL) } u := &url.URL{ Scheme: scheme, Host: host} return u.String() } func (c Client) getEndpoint(service, path string, params url.Values) string { u, err := url.Parse(c.getBaseURL(service)) if err != nil { // really should not be happening panic(err) } // API doesn't accept path segments not starting with '/' if !strings.HasPrefix(path, "/") { path = fmt.Sprintf("/%v", path) } if c.accountName == StorageEmulatorAccountName { path = fmt.Sprintf("/%v%v", StorageEmulatorAccountName, path) } u.Path = path u.RawQuery = params.Encode() return u.String() } // GetBlobService returns a BlobStorageClient which can operate on the blob // service of the storage account. func (c Client) GetBlobService() BlobStorageClient { return BlobStorageClient{c} } // GetQueueService returns a QueueServiceClient which can operate on the queue // service of the storage account. func (c Client) GetQueueService() QueueServiceClient { return QueueServiceClient{c} } // GetTableService returns a TableServiceClient which can operate on the table // service of the storage account. func (c Client) GetTableService() TableServiceClient { return TableServiceClient{c} } // GetFileService returns a FileServiceClient which can operate on the file // service of the storage account. func (c Client) GetFileService() FileServiceClient { return FileServiceClient{c} } func (c Client) createAuthorizationHeader(canonicalizedString string) string { signature := c.computeHmac256(canonicalizedString) return fmt.Sprintf("%s %s:%s", "SharedKey", c.getCanonicalizedAccountName(), signature) } func (c Client) getAuthorizationHeader(verb, url string, headers map[string]string) (string, error) { canonicalizedResource, err := c.buildCanonicalizedResource(url) if err != nil { return "", err } canonicalizedString := c.buildCanonicalizedString(verb, headers, canonicalizedResource) return c.createAuthorizationHeader(canonicalizedString), nil } func (c Client) getStandardHeaders() map[string]string { return map[string]string{ "x-ms-version": c.apiVersion, "x-ms-date": currentTimeRfc1123Formatted(), } } func (c Client) getCanonicalizedAccountName() string { // since we may be trying to access a secondary storage account, we need to // remove the -secondary part of the storage name return strings.TrimSuffix(c.accountName, "-secondary") } func (c Client) buildCanonicalizedHeader(headers map[string]string) string { cm := make(map[string]string) for k, v := range headers { headerName := strings.TrimSpace(strings.ToLower(k)) match, _ := regexp.MatchString("x-ms-", headerName) if match { cm[headerName] = v } } if len(cm) == 0 { return "" } keys := make([]string, 0, len(cm)) for key := range cm { keys = append(keys, key) } sort.Strings(keys) ch := "" for i, key := range keys { if i == len(keys)-1 { ch += fmt.Sprintf("%s:%s", key, cm[key]) } else { ch += fmt.Sprintf("%s:%s\n", key, cm[key]) } } return ch } func (c Client) buildCanonicalizedResourceTable(uri string) (string, error) { errMsg := "buildCanonicalizedResourceTable error: %s" u, err := url.Parse(uri) if err != nil { return "", fmt.Errorf(errMsg, err.Error()) } cr := "/" + c.getCanonicalizedAccountName() if len(u.Path) > 0 { cr += u.Path } return cr, nil } func (c Client) buildCanonicalizedResource(uri string) (string, error) { errMsg := "buildCanonicalizedResource error: %s" u, err := url.Parse(uri) if err != nil { return "", fmt.Errorf(errMsg, err.Error()) } cr := "/" + c.getCanonicalizedAccountName() if len(u.Path) > 0 { // Any portion of the CanonicalizedResource string that is derived from // the resource's URI should be encoded exactly as it is in the URI. // -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx cr += u.EscapedPath() } params, err := url.ParseQuery(u.RawQuery) if err != nil { return "", fmt.Errorf(errMsg, err.Error()) } if len(params) > 0 { cr += "\n" keys := make([]string, 0, len(params)) for key := range params { keys = append(keys, key) } sort.Strings(keys) for i, key := range keys { if len(params[key]) > 1 { sort.Strings(params[key]) } if i == len(keys)-1 { cr += fmt.Sprintf("%s:%s", key, strings.Join(params[key], ",")) } else { cr += fmt.Sprintf("%s:%s\n", key, strings.Join(params[key], ",")) } } } return cr, nil } func (c Client) buildCanonicalizedString(verb string, headers map[string]string, canonicalizedResource string) string { contentLength := headers["Content-Length"] if contentLength == "0" { contentLength = "" } canonicalizedString := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s", verb, headers["Content-Encoding"], headers["Content-Language"], contentLength, headers["Content-MD5"], headers["Content-Type"], headers["Date"], headers["If-Modified-Since"], headers["If-Match"], headers["If-None-Match"], headers["If-Unmodified-Since"], headers["Range"], c.buildCanonicalizedHeader(headers), canonicalizedResource) return canonicalizedString } func (c Client) exec(verb, url string, headers map[string]string, body io.Reader) (*storageResponse, error) { authHeader, err := c.getAuthorizationHeader(verb, url, headers) if err != nil { return nil, err } headers["Authorization"] = authHeader if err != nil { return nil, err } req, err := http.NewRequest(verb, url, body) if err != nil { return nil, errors.New("azure/storage: error creating request: " + err.Error()) } if clstr, ok := headers["Content-Length"]; ok { // content length header is being signed, but completely ignored by golang. // instead we have to use the ContentLength property on the request struct // (see https://golang.org/src/net/http/request.go?s=18140:18370#L536 and // https://golang.org/src/net/http/transfer.go?s=1739:2467#L49) req.ContentLength, err = strconv.ParseInt(clstr, 10, 64) if err != nil { return nil, err } } for k, v := range headers { req.Header.Add(k, v) } httpClient := c.HTTPClient if httpClient == nil { httpClient = http.DefaultClient } resp, err := httpClient.Do(req) if err != nil { return nil, err } statusCode := resp.StatusCode if statusCode >= 400 && statusCode <= 505 { var respBody []byte respBody, err = readResponseBody(resp) if err != nil { return nil, err } if len(respBody) == 0 { // no error in response body err = fmt.Errorf("storage: service returned without a response body (%s)", resp.Status) } else { // response contains storage service error object, unmarshal storageErr, errIn := serviceErrFromXML(respBody, resp.StatusCode, resp.Header.Get("x-ms-request-id")) if err != nil { // error unmarshaling the error response err = errIn } err = storageErr } return &storageResponse{ statusCode: resp.StatusCode, headers: resp.Header, body: ioutil.NopCloser(bytes.NewReader(respBody)), /* restore the body */ }, err } return &storageResponse{ statusCode: resp.StatusCode, headers: resp.Header, body: resp.Body}, nil } func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { req, err := http.NewRequest(verb, url, body) for k, v := range headers { req.Header.Add(k, v) } httpClient := c.HTTPClient if httpClient == nil { httpClient = http.DefaultClient } resp, err := httpClient.Do(req) if err != nil { return nil, err } respToRet := &odataResponse{} respToRet.body = resp.Body respToRet.statusCode = resp.StatusCode respToRet.headers = resp.Header statusCode := resp.StatusCode if statusCode >= 400 && statusCode <= 505 { var respBody []byte respBody, err = readResponseBody(resp) if err != nil { return nil, err } if len(respBody) == 0 { // no error in response body err = fmt.Errorf("storage: service returned without a response body (%d)", resp.StatusCode) return respToRet, err } // try unmarshal as odata.error json err = json.Unmarshal(respBody, &respToRet.odata) return respToRet, err } return respToRet, nil } func (c Client) createSharedKeyLite(url string, headers map[string]string) (string, error) { can, err := c.buildCanonicalizedResourceTable(url) if err != nil { return "", err } strToSign := headers["x-ms-date"] + "\n" + can hmac := c.computeHmac256(strToSign) return fmt.Sprintf("SharedKeyLite %s:%s", c.accountName, hmac), nil } func (c Client) execTable(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { var err error headers["Authorization"], err = c.createSharedKeyLite(url, headers) if err != nil { return nil, err } return c.execInternalJSON(verb, url, headers, body) } func readResponseBody(resp *http.Response) ([]byte, error) { defer resp.Body.Close() out, err := ioutil.ReadAll(resp.Body) if err == io.EOF { err = nil } return out, err } func serviceErrFromXML(body []byte, statusCode int, requestID string) (AzureStorageServiceError, error) { var storageErr AzureStorageServiceError if err := xml.Unmarshal(body, &storageErr); err != nil { return storageErr, err } storageErr.StatusCode = statusCode storageErr.RequestID = requestID return storageErr, nil } func (e AzureStorageServiceError) Error() string { return fmt.Sprintf("storage: service returned error: StatusCode=%d, ErrorCode=%s, ErrorMessage=%s, RequestId=%s, QueryParameterName=%s, QueryParameterValue=%s", e.StatusCode, e.Code, e.Message, e.RequestID, e.QueryParameterName, e.QueryParameterValue) } // checkRespCode returns UnexpectedStatusError if the given response code is not // one of the allowed status codes; otherwise nil. func checkRespCode(respCode int, allowed []int) error { for _, v := range allowed { if respCode == v { return nil } } return UnexpectedStatusCodeError{allowed, respCode} } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/file.go000066400000000000000000000263161313450123100300750ustar00rootroot00000000000000package storage import ( "encoding/xml" "fmt" "net/http" "net/url" "strings" ) // FileServiceClient contains operations for Microsoft Azure File Service. type FileServiceClient struct { client Client } // A Share is an entry in ShareListResponse. type Share struct { Name string `xml:"Name"` Properties ShareProperties `xml:"Properties"` } // ShareProperties contains various properties of a share returned from // various endpoints like ListShares. type ShareProperties struct { LastModified string `xml:"Last-Modified"` Etag string `xml:"Etag"` Quota string `xml:"Quota"` } // ShareListResponse contains the response fields from // ListShares call. // // See https://msdn.microsoft.com/en-us/library/azure/dn167009.aspx type ShareListResponse struct { XMLName xml.Name `xml:"EnumerationResults"` Xmlns string `xml:"xmlns,attr"` Prefix string `xml:"Prefix"` Marker string `xml:"Marker"` NextMarker string `xml:"NextMarker"` MaxResults int64 `xml:"MaxResults"` Shares []Share `xml:"Shares>Share"` } // ListSharesParameters defines the set of customizable parameters to make a // List Shares call. // // See https://msdn.microsoft.com/en-us/library/azure/dn167009.aspx type ListSharesParameters struct { Prefix string Marker string Include string MaxResults uint Timeout uint } // ShareHeaders contains various properties of a file and is an entry // in SetShareProperties type ShareHeaders struct { Quota string `header:"x-ms-share-quota"` } func (p ListSharesParameters) getParameters() url.Values { out := url.Values{} if p.Prefix != "" { out.Set("prefix", p.Prefix) } if p.Marker != "" { out.Set("marker", p.Marker) } if p.Include != "" { out.Set("include", p.Include) } if p.MaxResults != 0 { out.Set("maxresults", fmt.Sprintf("%v", p.MaxResults)) } if p.Timeout != 0 { out.Set("timeout", fmt.Sprintf("%v", p.Timeout)) } return out } // pathForFileShare returns the URL path segment for a File Share resource func pathForFileShare(name string) string { return fmt.Sprintf("/%s", name) } // ListShares returns the list of shares in a storage account along with // pagination token and other response details. // // See https://msdn.microsoft.com/en-us/library/azure/dd179352.aspx func (f FileServiceClient) ListShares(params ListSharesParameters) (ShareListResponse, error) { q := mergeParams(params.getParameters(), url.Values{"comp": {"list"}}) uri := f.client.getEndpoint(fileServiceName, "", q) headers := f.client.getStandardHeaders() var out ShareListResponse resp, err := f.client.exec("GET", uri, headers, nil) if err != nil { return out, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &out) return out, err } // CreateShare operation creates a new share under the specified account. If the // share with the same name already exists, the operation fails. // // See https://msdn.microsoft.com/en-us/library/azure/dn167008.aspx func (f FileServiceClient) CreateShare(name string) error { resp, err := f.createShare(name) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // ShareExists returns true if a share with given name exists // on the storage account, otherwise returns false. func (f FileServiceClient) ShareExists(name string) (bool, error) { uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}}) headers := f.client.getStandardHeaders() resp, err := f.client.exec("HEAD", uri, headers, nil) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusOK, nil } } return false, err } // GetShareURL gets the canonical URL to the share with the specified name in the // specified container. This method does not create a publicly accessible URL if // the file is private and this method does not check if the file // exists. func (f FileServiceClient) GetShareURL(name string) string { return f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{}) } // CreateShareIfNotExists creates a new share under the specified account if // it does not exist. Returns true if container is newly created or false if // container already exists. // // See https://msdn.microsoft.com/en-us/library/azure/dn167008.aspx func (f FileServiceClient) CreateShareIfNotExists(name string) (bool, error) { resp, err := f.createShare(name) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusCreated || resp.statusCode == http.StatusConflict { return resp.statusCode == http.StatusCreated, nil } } return false, err } // CreateShare creates a Azure File Share and returns its response func (f FileServiceClient) createShare(name string) (*storageResponse, error) { if err := f.checkForStorageEmulator(); err != nil { return nil, err } uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}}) headers := f.client.getStandardHeaders() return f.client.exec("PUT", uri, headers, nil) } // GetShareProperties provides various information about the specified // file. See https://msdn.microsoft.com/en-us/library/azure/dn689099.aspx func (f FileServiceClient) GetShareProperties(name string) (*ShareProperties, error) { uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}}) headers := f.client.getStandardHeaders() resp, err := f.client.exec("HEAD", uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } return &ShareProperties{ LastModified: resp.headers.Get("Last-Modified"), Etag: resp.headers.Get("Etag"), Quota: resp.headers.Get("x-ms-share-quota"), }, nil } // SetShareProperties replaces the ShareHeaders for the specified file. // // Some keys may be converted to Camel-Case before sending. All keys // are returned in lower case by SetShareProperties. HTTP header names // are case-insensitive so case munging should not matter to other // applications either. // // See https://msdn.microsoft.com/en-us/library/azure/mt427368.aspx func (f FileServiceClient) SetShareProperties(name string, shareHeaders ShareHeaders) error { params := url.Values{} params.Set("restype", "share") params.Set("comp", "properties") uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params) headers := f.client.getStandardHeaders() extraHeaders := headersFromStruct(shareHeaders) for k, v := range extraHeaders { headers[k] = v } resp, err := f.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusOK}) } // DeleteShare operation marks the specified share for deletion. The share // and any files contained within it are later deleted during garbage // collection. // // See https://msdn.microsoft.com/en-us/library/azure/dn689090.aspx func (f FileServiceClient) DeleteShare(name string) error { resp, err := f.deleteShare(name) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusAccepted}) } // DeleteShareIfExists operation marks the specified share for deletion if it // exists. The share and any files contained within it are later deleted during // garbage collection. Returns true if share existed and deleted with this call, // false otherwise. // // See https://msdn.microsoft.com/en-us/library/azure/dn689090.aspx func (f FileServiceClient) DeleteShareIfExists(name string) (bool, error) { resp, err := f.deleteShare(name) if resp != nil { defer resp.body.Close() if resp.statusCode == http.StatusAccepted || resp.statusCode == http.StatusNotFound { return resp.statusCode == http.StatusAccepted, nil } } return false, err } // deleteShare makes the call to Delete Share operation endpoint and returns // the response func (f FileServiceClient) deleteShare(name string) (*storageResponse, error) { if err := f.checkForStorageEmulator(); err != nil { return nil, err } uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), url.Values{"restype": {"share"}}) return f.client.exec("DELETE", uri, f.client.getStandardHeaders(), nil) } // SetShareMetadata replaces the metadata for the specified Share. // // Some keys may be converted to Camel-Case before sending. All keys // are returned in lower case by GetShareMetadata. HTTP header names // are case-insensitive so case munging should not matter to other // applications either. // // See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx func (f FileServiceClient) SetShareMetadata(name string, metadata map[string]string, extraHeaders map[string]string) error { params := url.Values{} params.Set("restype", "share") params.Set("comp", "metadata") uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params) headers := f.client.getStandardHeaders() for k, v := range metadata { headers[userDefinedMetadataHeaderPrefix+k] = v } for k, v := range extraHeaders { headers[k] = v } resp, err := f.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusOK}) } // GetShareMetadata returns all user-defined metadata for the specified share. // // All metadata keys will be returned in lower case. (HTTP header // names are case-insensitive.) // // See https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx func (f FileServiceClient) GetShareMetadata(name string) (map[string]string, error) { params := url.Values{} params.Set("restype", "share") params.Set("comp", "metadata") uri := f.client.getEndpoint(fileServiceName, pathForFileShare(name), params) headers := f.client.getStandardHeaders() resp, err := f.client.exec("GET", uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } metadata := make(map[string]string) for k, v := range resp.headers { // Can't trust CanonicalHeaderKey() to munge case // reliably. "_" is allowed in identifiers: // https://msdn.microsoft.com/en-us/library/azure/dd179414.aspx // https://msdn.microsoft.com/library/aa664670(VS.71).aspx // http://tools.ietf.org/html/rfc7230#section-3.2 // ...but "_" is considered invalid by // CanonicalMIMEHeaderKey in // https://golang.org/src/net/textproto/reader.go?s=14615:14659#L542 // so k can be "X-Ms-Meta-Foo" or "x-ms-meta-foo_bar". k = strings.ToLower(k) if len(v) == 0 || !strings.HasPrefix(k, strings.ToLower(userDefinedMetadataHeaderPrefix)) { continue } // metadata["foo"] = content of the last X-Ms-Meta-Foo header k = k[len(userDefinedMetadataHeaderPrefix):] metadata[k] = v[len(v)-1] } return metadata, nil } //checkForStorageEmulator determines if the client is setup for use with //Azure Storage Emulator, and returns a relevant error func (f FileServiceClient) checkForStorageEmulator() error { if f.client.accountName == StorageEmulatorAccountName { return fmt.Errorf("Error: File service is not currently supported by Azure Storage Emulator") } return nil } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/queue.go000066400000000000000000000246141313450123100303010ustar00rootroot00000000000000package storage import ( "encoding/xml" "fmt" "net/http" "net/url" "strconv" "strings" ) const ( // casing is per Golang's http.Header canonicalizing the header names. approximateMessagesCountHeader = "X-Ms-Approximate-Messages-Count" userDefinedMetadataHeaderPrefix = "X-Ms-Meta-" ) // QueueServiceClient contains operations for Microsoft Azure Queue Storage // Service. type QueueServiceClient struct { client Client } func pathForQueue(queue string) string { return fmt.Sprintf("/%s", queue) } func pathForQueueMessages(queue string) string { return fmt.Sprintf("/%s/messages", queue) } func pathForMessage(queue, name string) string { return fmt.Sprintf("/%s/messages/%s", queue, name) } type putMessageRequest struct { XMLName xml.Name `xml:"QueueMessage"` MessageText string `xml:"MessageText"` } // PutMessageParameters is the set of options can be specified for Put Messsage // operation. A zero struct does not use any preferences for the request. type PutMessageParameters struct { VisibilityTimeout int MessageTTL int } func (p PutMessageParameters) getParameters() url.Values { out := url.Values{} if p.VisibilityTimeout != 0 { out.Set("visibilitytimeout", strconv.Itoa(p.VisibilityTimeout)) } if p.MessageTTL != 0 { out.Set("messagettl", strconv.Itoa(p.MessageTTL)) } return out } // GetMessagesParameters is the set of options can be specified for Get // Messsages operation. A zero struct does not use any preferences for the // request. type GetMessagesParameters struct { NumOfMessages int VisibilityTimeout int } func (p GetMessagesParameters) getParameters() url.Values { out := url.Values{} if p.NumOfMessages != 0 { out.Set("numofmessages", strconv.Itoa(p.NumOfMessages)) } if p.VisibilityTimeout != 0 { out.Set("visibilitytimeout", strconv.Itoa(p.VisibilityTimeout)) } return out } // PeekMessagesParameters is the set of options can be specified for Peek // Messsage operation. A zero struct does not use any preferences for the // request. type PeekMessagesParameters struct { NumOfMessages int } func (p PeekMessagesParameters) getParameters() url.Values { out := url.Values{"peekonly": {"true"}} // Required for peek operation if p.NumOfMessages != 0 { out.Set("numofmessages", strconv.Itoa(p.NumOfMessages)) } return out } // GetMessagesResponse represents a response returned from Get Messages // operation. type GetMessagesResponse struct { XMLName xml.Name `xml:"QueueMessagesList"` QueueMessagesList []GetMessageResponse `xml:"QueueMessage"` } // GetMessageResponse represents a QueueMessage object returned from Get // Messages operation response. type GetMessageResponse struct { MessageID string `xml:"MessageId"` InsertionTime string `xml:"InsertionTime"` ExpirationTime string `xml:"ExpirationTime"` PopReceipt string `xml:"PopReceipt"` TimeNextVisible string `xml:"TimeNextVisible"` DequeueCount int `xml:"DequeueCount"` MessageText string `xml:"MessageText"` } // PeekMessagesResponse represents a response returned from Get Messages // operation. type PeekMessagesResponse struct { XMLName xml.Name `xml:"QueueMessagesList"` QueueMessagesList []PeekMessageResponse `xml:"QueueMessage"` } // PeekMessageResponse represents a QueueMessage object returned from Peek // Messages operation response. type PeekMessageResponse struct { MessageID string `xml:"MessageId"` InsertionTime string `xml:"InsertionTime"` ExpirationTime string `xml:"ExpirationTime"` DequeueCount int `xml:"DequeueCount"` MessageText string `xml:"MessageText"` } // QueueMetadataResponse represents user defined metadata and queue // properties on a specific queue. // // See https://msdn.microsoft.com/en-us/library/azure/dd179384.aspx type QueueMetadataResponse struct { ApproximateMessageCount int UserDefinedMetadata map[string]string } // SetMetadata operation sets user-defined metadata on the specified queue. // Metadata is associated with the queue as name-value pairs. // // See https://msdn.microsoft.com/en-us/library/azure/dd179348.aspx func (c QueueServiceClient) SetMetadata(name string, metadata map[string]string) error { uri := c.client.getEndpoint(queueServiceName, pathForQueue(name), url.Values{"comp": []string{"metadata"}}) headers := c.client.getStandardHeaders() for k, v := range metadata { headers[userDefinedMetadataHeaderPrefix+k] = v } resp, err := c.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusNoContent}) } // GetMetadata operation retrieves user-defined metadata and queue // properties on the specified queue. Metadata is associated with // the queue as name-values pairs. // // See https://msdn.microsoft.com/en-us/library/azure/dd179384.aspx // // Because the way Golang's http client (and http.Header in particular) // canonicalize header names, the returned metadata names would always // be all lower case. func (c QueueServiceClient) GetMetadata(name string) (QueueMetadataResponse, error) { qm := QueueMetadataResponse{} qm.UserDefinedMetadata = make(map[string]string) uri := c.client.getEndpoint(queueServiceName, pathForQueue(name), url.Values{"comp": []string{"metadata"}}) headers := c.client.getStandardHeaders() resp, err := c.client.exec("GET", uri, headers, nil) if err != nil { return qm, err } defer resp.body.Close() for k, v := range resp.headers { if len(v) != 1 { return qm, fmt.Errorf("Unexpected number of values (%d) in response header '%s'", len(v), k) } value := v[0] if k == approximateMessagesCountHeader { qm.ApproximateMessageCount, err = strconv.Atoi(value) if err != nil { return qm, fmt.Errorf("Unexpected value in response header '%s': '%s' ", k, value) } } else if strings.HasPrefix(k, userDefinedMetadataHeaderPrefix) { name := strings.TrimPrefix(k, userDefinedMetadataHeaderPrefix) qm.UserDefinedMetadata[strings.ToLower(name)] = value } } return qm, checkRespCode(resp.statusCode, []int{http.StatusOK}) } // CreateQueue operation creates a queue under the given account. // // See https://msdn.microsoft.com/en-us/library/azure/dd179342.aspx func (c QueueServiceClient) CreateQueue(name string) error { uri := c.client.getEndpoint(queueServiceName, pathForQueue(name), url.Values{}) headers := c.client.getStandardHeaders() resp, err := c.client.exec("PUT", uri, headers, nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // DeleteQueue operation permanently deletes the specified queue. // // See https://msdn.microsoft.com/en-us/library/azure/dd179436.aspx func (c QueueServiceClient) DeleteQueue(name string) error { uri := c.client.getEndpoint(queueServiceName, pathForQueue(name), url.Values{}) resp, err := c.client.exec("DELETE", uri, c.client.getStandardHeaders(), nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusNoContent}) } // QueueExists returns true if a queue with given name exists. func (c QueueServiceClient) QueueExists(name string) (bool, error) { uri := c.client.getEndpoint(queueServiceName, pathForQueue(name), url.Values{"comp": {"metadata"}}) resp, err := c.client.exec("GET", uri, c.client.getStandardHeaders(), nil) if resp != nil && (resp.statusCode == http.StatusOK || resp.statusCode == http.StatusNotFound) { return resp.statusCode == http.StatusOK, nil } return false, err } // PutMessage operation adds a new message to the back of the message queue. // // See https://msdn.microsoft.com/en-us/library/azure/dd179346.aspx func (c QueueServiceClient) PutMessage(queue string, message string, params PutMessageParameters) error { uri := c.client.getEndpoint(queueServiceName, pathForQueueMessages(queue), params.getParameters()) req := putMessageRequest{MessageText: message} body, nn, err := xmlMarshal(req) if err != nil { return err } headers := c.client.getStandardHeaders() headers["Content-Length"] = strconv.Itoa(nn) resp, err := c.client.exec("POST", uri, headers, body) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusCreated}) } // ClearMessages operation deletes all messages from the specified queue. // // See https://msdn.microsoft.com/en-us/library/azure/dd179454.aspx func (c QueueServiceClient) ClearMessages(queue string) error { uri := c.client.getEndpoint(queueServiceName, pathForQueueMessages(queue), url.Values{}) resp, err := c.client.exec("DELETE", uri, c.client.getStandardHeaders(), nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusNoContent}) } // GetMessages operation retrieves one or more messages from the front of the // queue. // // See https://msdn.microsoft.com/en-us/library/azure/dd179474.aspx func (c QueueServiceClient) GetMessages(queue string, params GetMessagesParameters) (GetMessagesResponse, error) { var r GetMessagesResponse uri := c.client.getEndpoint(queueServiceName, pathForQueueMessages(queue), params.getParameters()) resp, err := c.client.exec("GET", uri, c.client.getStandardHeaders(), nil) if err != nil { return r, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &r) return r, err } // PeekMessages retrieves one or more messages from the front of the queue, but // does not alter the visibility of the message. // // See https://msdn.microsoft.com/en-us/library/azure/dd179472.aspx func (c QueueServiceClient) PeekMessages(queue string, params PeekMessagesParameters) (PeekMessagesResponse, error) { var r PeekMessagesResponse uri := c.client.getEndpoint(queueServiceName, pathForQueueMessages(queue), params.getParameters()) resp, err := c.client.exec("GET", uri, c.client.getStandardHeaders(), nil) if err != nil { return r, err } defer resp.body.Close() err = xmlUnmarshal(resp.body, &r) return r, err } // DeleteMessage operation deletes the specified message. // // See https://msdn.microsoft.com/en-us/library/azure/dd179347.aspx func (c QueueServiceClient) DeleteMessage(queue, messageID, popReceipt string) error { uri := c.client.getEndpoint(queueServiceName, pathForMessage(queue, messageID), url.Values{ "popreceipt": {popReceipt}}) resp, err := c.client.exec("DELETE", uri, c.client.getStandardHeaders(), nil) if err != nil { return err } defer resp.body.Close() return checkRespCode(resp.statusCode, []int{http.StatusNoContent}) } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/table.go000066400000000000000000000060771313450123100302470ustar00rootroot00000000000000package storage import ( "bytes" "encoding/json" "fmt" "net/http" "net/url" ) // TableServiceClient contains operations for Microsoft Azure Table Storage // Service. type TableServiceClient struct { client Client } // AzureTable is the typedef of the Azure Table name type AzureTable string const ( tablesURIPath = "/Tables" ) type createTableRequest struct { TableName string `json:"TableName"` } func pathForTable(table AzureTable) string { return fmt.Sprintf("%s", table) } func (c *TableServiceClient) getStandardHeaders() map[string]string { return map[string]string{ "x-ms-version": "2015-02-21", "x-ms-date": currentTimeRfc1123Formatted(), "Accept": "application/json;odata=nometadata", "Accept-Charset": "UTF-8", "Content-Type": "application/json", } } // QueryTables returns the tables created in the // *TableServiceClient storage account. func (c *TableServiceClient) QueryTables() ([]AzureTable, error) { uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) headers := c.getStandardHeaders() headers["Content-Length"] = "0" resp, err := c.client.execTable("GET", uri, headers, nil) if err != nil { return nil, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, err } buf := new(bytes.Buffer) buf.ReadFrom(resp.body) var respArray queryTablesResponse if err := json.Unmarshal(buf.Bytes(), &respArray); err != nil { return nil, err } s := make([]AzureTable, len(respArray.TableName)) for i, elem := range respArray.TableName { s[i] = AzureTable(elem.TableName) } return s, nil } // CreateTable creates the table given the specific // name. This function fails if the name is not compliant // with the specification or the tables already exists. func (c *TableServiceClient) CreateTable(table AzureTable) error { uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) headers := c.getStandardHeaders() req := createTableRequest{TableName: string(table)} buf := new(bytes.Buffer) if err := json.NewEncoder(buf).Encode(req); err != nil { return err } headers["Content-Length"] = fmt.Sprintf("%d", buf.Len()) resp, err := c.client.execTable("POST", uri, headers, buf) if err != nil { return err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil { return err } return nil } // DeleteTable deletes the table given the specific // name. This function fails if the table is not present. // Be advised: DeleteTable deletes all the entries // that may be present. func (c *TableServiceClient) DeleteTable(table AzureTable) error { uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) uri += fmt.Sprintf("('%s')", string(table)) headers := c.getStandardHeaders() headers["Content-Length"] = "0" resp, err := c.client.execTable("DELETE", uri, headers, nil) if err != nil { return err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil { return err } return nil } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/table_entities.go000066400000000000000000000241701313450123100321450ustar00rootroot00000000000000package storage import ( "bytes" "encoding/json" "fmt" "io" "net/http" "net/url" "reflect" ) const ( partitionKeyNode = "PartitionKey" rowKeyNode = "RowKey" tag = "table" tagIgnore = "-" continuationTokenPartitionKeyHeader = "X-Ms-Continuation-Nextpartitionkey" continuationTokenRowHeader = "X-Ms-Continuation-Nextrowkey" maxTopParameter = 1000 ) type queryTablesResponse struct { TableName []struct { TableName string `json:"TableName"` } `json:"value"` } const ( tableOperationTypeInsert = iota tableOperationTypeUpdate = iota tableOperationTypeMerge = iota tableOperationTypeInsertOrReplace = iota tableOperationTypeInsertOrMerge = iota ) type tableOperation int // TableEntity interface specifies // the functions needed to support // marshaling and unmarshaling into // Azure Tables. The struct must only contain // simple types because Azure Tables do not // support hierarchy. type TableEntity interface { PartitionKey() string RowKey() string SetPartitionKey(string) error SetRowKey(string) error } // ContinuationToken is an opaque (ie not useful to inspect) // struct that Get... methods can return if there are more // entries to be returned than the ones already // returned. Just pass it to the same function to continue // receiving the remaining entries. type ContinuationToken struct { NextPartitionKey string NextRowKey string } type getTableEntriesResponse struct { Elements []map[string]interface{} `json:"value"` } // QueryTableEntities queries the specified table and returns the unmarshaled // entities of type retType. // top parameter limits the returned entries up to top. Maximum top // allowed by Azure API is 1000. In case there are more than top entries to be // returned the function will return a non nil *ContinuationToken. You can call the // same function again passing the received ContinuationToken as previousContToken // parameter in order to get the following entries. The query parameter // is the odata query. To retrieve all the entries pass the empty string. // The function returns a pointer to a TableEntity slice, the *ContinuationToken // if there are more entries to be returned and an error in case something went // wrong. // // Example: // entities, cToken, err = tSvc.QueryTableEntities("table", cToken, reflect.TypeOf(entity), 20, "") func (c *TableServiceClient) QueryTableEntities(tableName AzureTable, previousContToken *ContinuationToken, retType reflect.Type, top int, query string) ([]TableEntity, *ContinuationToken, error) { if top > maxTopParameter { return nil, nil, fmt.Errorf("top accepts at maximum %d elements. Requested %d instead", maxTopParameter, top) } uri := c.client.getEndpoint(tableServiceName, pathForTable(tableName), url.Values{}) uri += fmt.Sprintf("?$top=%d", top) if query != "" { uri += fmt.Sprintf("&$filter=%s", url.QueryEscape(query)) } if previousContToken != nil { uri += fmt.Sprintf("&NextPartitionKey=%s&NextRowKey=%s", previousContToken.NextPartitionKey, previousContToken.NextRowKey) } headers := c.getStandardHeaders() headers["Content-Length"] = "0" resp, err := c.client.execTable("GET", uri, headers, nil) if err != nil { return nil, nil, err } contToken := extractContinuationTokenFromHeaders(resp.headers) if err != nil { return nil, contToken, err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { return nil, contToken, err } retEntries, err := deserializeEntity(retType, resp.body) if err != nil { return nil, contToken, err } return retEntries, contToken, nil } // InsertEntity inserts an entity in the specified table. // The function fails if there is an entity with the same // PartitionKey and RowKey in the table. func (c *TableServiceClient) InsertEntity(table AzureTable, entity TableEntity) error { var err error if sc, err := c.execTable(table, entity, false, "POST"); err != nil { return checkRespCode(sc, []int{http.StatusCreated}) } return err } func (c *TableServiceClient) execTable(table AzureTable, entity TableEntity, specifyKeysInURL bool, method string) (int, error) { uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{}) if specifyKeysInURL { uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey())) } headers := c.getStandardHeaders() var buf bytes.Buffer if err := injectPartitionAndRowKeys(entity, &buf); err != nil { return 0, err } headers["Content-Length"] = fmt.Sprintf("%d", buf.Len()) var err error var resp *odataResponse resp, err = c.client.execTable(method, uri, headers, &buf) if err != nil { return 0, err } defer resp.body.Close() return resp.statusCode, nil } // UpdateEntity updates the contents of an entity with the // one passed as parameter. The function fails if there is no entity // with the same PartitionKey and RowKey in the table. func (c *TableServiceClient) UpdateEntity(table AzureTable, entity TableEntity) error { var err error if sc, err := c.execTable(table, entity, true, "PUT"); err != nil { return checkRespCode(sc, []int{http.StatusNoContent}) } return err } // MergeEntity merges the contents of an entity with the // one passed as parameter. // The function fails if there is no entity // with the same PartitionKey and RowKey in the table. func (c *TableServiceClient) MergeEntity(table AzureTable, entity TableEntity) error { var err error if sc, err := c.execTable(table, entity, true, "MERGE"); err != nil { return checkRespCode(sc, []int{http.StatusNoContent}) } return err } // DeleteEntityWithoutCheck deletes the entity matching by // PartitionKey and RowKey. There is no check on IfMatch // parameter so the entity is always deleted. // The function fails if there is no entity // with the same PartitionKey and RowKey in the table. func (c *TableServiceClient) DeleteEntityWithoutCheck(table AzureTable, entity TableEntity) error { return c.DeleteEntity(table, entity, "*") } // DeleteEntity deletes the entity matching by // PartitionKey, RowKey and ifMatch field. // The function fails if there is no entity // with the same PartitionKey and RowKey in the table or // the ifMatch is different. func (c *TableServiceClient) DeleteEntity(table AzureTable, entity TableEntity, ifMatch string) error { uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{}) uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey())) headers := c.getStandardHeaders() headers["Content-Length"] = "0" headers["If-Match"] = ifMatch resp, err := c.client.execTable("DELETE", uri, headers, nil) if err != nil { return err } defer resp.body.Close() if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil { return err } return nil } // InsertOrReplaceEntity inserts an entity in the specified table // or replaced the existing one. func (c *TableServiceClient) InsertOrReplaceEntity(table AzureTable, entity TableEntity) error { var err error if sc, err := c.execTable(table, entity, true, "PUT"); err != nil { return checkRespCode(sc, []int{http.StatusNoContent}) } return err } // InsertOrMergeEntity inserts an entity in the specified table // or merges the existing one. func (c *TableServiceClient) InsertOrMergeEntity(table AzureTable, entity TableEntity) error { var err error if sc, err := c.execTable(table, entity, true, "MERGE"); err != nil { return checkRespCode(sc, []int{http.StatusNoContent}) } return err } func injectPartitionAndRowKeys(entity TableEntity, buf *bytes.Buffer) error { if err := json.NewEncoder(buf).Encode(entity); err != nil { return err } dec := make(map[string]interface{}) if err := json.NewDecoder(buf).Decode(&dec); err != nil { return err } // Inject PartitionKey and RowKey dec[partitionKeyNode] = entity.PartitionKey() dec[rowKeyNode] = entity.RowKey() // Remove tagged fields // The tag is defined in the const section // This is useful to avoid storing the PartitionKey and RowKey twice. numFields := reflect.ValueOf(entity).Elem().NumField() for i := 0; i < numFields; i++ { f := reflect.ValueOf(entity).Elem().Type().Field(i) if f.Tag.Get(tag) == tagIgnore { // we must look for its JSON name in the dictionary // as the user can rename it using a tag jsonName := f.Name if f.Tag.Get("json") != "" { jsonName = f.Tag.Get("json") } delete(dec, jsonName) } } buf.Reset() if err := json.NewEncoder(buf).Encode(&dec); err != nil { return err } return nil } func deserializeEntity(retType reflect.Type, reader io.Reader) ([]TableEntity, error) { buf := new(bytes.Buffer) var ret getTableEntriesResponse if err := json.NewDecoder(reader).Decode(&ret); err != nil { return nil, err } tEntries := make([]TableEntity, len(ret.Elements)) for i, entry := range ret.Elements { buf.Reset() if err := json.NewEncoder(buf).Encode(entry); err != nil { return nil, err } dec := make(map[string]interface{}) if err := json.NewDecoder(buf).Decode(&dec); err != nil { return nil, err } var pKey, rKey string // strip pk and rk for key, val := range dec { switch key { case partitionKeyNode: pKey = val.(string) case rowKeyNode: rKey = val.(string) } } delete(dec, partitionKeyNode) delete(dec, rowKeyNode) buf.Reset() if err := json.NewEncoder(buf).Encode(dec); err != nil { return nil, err } // Create a empty retType instance tEntries[i] = reflect.New(retType.Elem()).Interface().(TableEntity) // Popolate it with the values if err := json.NewDecoder(buf).Decode(&tEntries[i]); err != nil { return nil, err } // Reset PartitionKey and RowKey tEntries[i].SetPartitionKey(pKey) tEntries[i].SetRowKey(rKey) } return tEntries, nil } func extractContinuationTokenFromHeaders(h http.Header) *ContinuationToken { ct := ContinuationToken{h.Get(continuationTokenPartitionKeyHeader), h.Get(continuationTokenRowHeader)} if ct.NextPartitionKey != "" && ct.NextRowKey != "" { return &ct } return nil } docker-registry-2.6.2~ds1/vendor/github.com/Azure/azure-sdk-for-go/storage/util.go000066400000000000000000000031771313450123100301330ustar00rootroot00000000000000package storage import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/xml" "fmt" "io" "io/ioutil" "net/http" "net/url" "reflect" "time" ) func (c Client) computeHmac256(message string) string { h := hmac.New(sha256.New, c.accountKey) h.Write([]byte(message)) return base64.StdEncoding.EncodeToString(h.Sum(nil)) } func currentTimeRfc1123Formatted() string { return timeRfc1123Formatted(time.Now().UTC()) } func timeRfc1123Formatted(t time.Time) string { return t.Format(http.TimeFormat) } func mergeParams(v1, v2 url.Values) url.Values { out := url.Values{} for k, v := range v1 { out[k] = v } for k, v := range v2 { vals, ok := out[k] if ok { vals = append(vals, v...) out[k] = vals } else { out[k] = v } } return out } func prepareBlockListRequest(blocks []Block) string { s := `` for _, v := range blocks { s += fmt.Sprintf("<%s>%s", v.Status, v.ID, v.Status) } s += `` return s } func xmlUnmarshal(body io.Reader, v interface{}) error { data, err := ioutil.ReadAll(body) if err != nil { return err } return xml.Unmarshal(data, v) } func xmlMarshal(v interface{}) (io.Reader, int, error) { b, err := xml.Marshal(v) if err != nil { return nil, 0, err } return bytes.NewReader(b), len(b), nil } func headersFromStruct(v interface{}) map[string]string { headers := make(map[string]string) value := reflect.ValueOf(v) for i := 0; i < value.NumField(); i++ { key := value.Type().Field(i).Tag.Get("header") val := value.Field(i).String() if val != "" { headers[key] = val } } return headers } docker-registry-2.6.2~ds1/vendor/github.com/docker/000077500000000000000000000000001313450123100222405ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/000077500000000000000000000000001313450123100233555ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/LICENSE000066400000000000000000000210531313450123100243630ustar00rootroot00000000000000This software is licensed under the LGPLv3, included below. As a special exception to the GNU Lesser General Public License version 3 ("LGPL3"), the copyright holders of this Library give you permission to convey to a third party a Combined Work that links statically or dynamically to this Library without providing any Minimal Corresponding Source or Minimal Application Code as set out in 4d or providing the installation information set out in section 4e, provided that you comply with the other provisions of LGPL3 and provided that you meet, for the Application the terms and conditions of the license(s) which apply to the Application. Except as stated in this special exception, the provisions of LGPL3 will continue to comply in full to this Library. If you modify this Library, you may apply this exception to your version of this Library, but you are not obliged to do so. If you do not wish to do so, delete this exception statement from your version. This exception does not (and cannot) modify any license terms which apply to the Application, with which you must still comply. GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/000077500000000000000000000000001313450123100241475ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/attempt.go000066400000000000000000000032641313450123100261610ustar00rootroot00000000000000package aws import ( "time" ) // AttemptStrategy represents a strategy for waiting for an action // to complete successfully. This is an internal type used by the // implementation of other goamz packages. type AttemptStrategy struct { Total time.Duration // total duration of attempt. Delay time.Duration // interval between each try in the burst. Min int // minimum number of retries; overrides Total } type Attempt struct { strategy AttemptStrategy last time.Time end time.Time force bool count int } // Start begins a new sequence of attempts for the given strategy. func (s AttemptStrategy) Start() *Attempt { now := time.Now() return &Attempt{ strategy: s, last: now, end: now.Add(s.Total), force: true, } } // Next waits until it is time to perform the next attempt or returns // false if it is time to stop trying. func (a *Attempt) Next() bool { now := time.Now() sleep := a.nextSleep(now) if !a.force && !now.Add(sleep).Before(a.end) && a.strategy.Min <= a.count { return false } a.force = false if sleep > 0 && a.count > 0 { time.Sleep(sleep) now = time.Now() } a.count++ a.last = now return true } func (a *Attempt) nextSleep(now time.Time) time.Duration { sleep := a.strategy.Delay - now.Sub(a.last) if sleep < 0 { return 0 } return sleep } // HasNext returns whether another attempt will be made if the current // one fails. If it returns true, the following call to Next is // guaranteed to return true. func (a *Attempt) HasNext() bool { if a.force || a.strategy.Min > a.count { return true } now := time.Now() if now.Add(a.nextSleep(now)).Before(a.end) { a.force = true return true } return false } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/aws.go000066400000000000000000000370251313450123100252770ustar00rootroot00000000000000// // goamz - Go packages to interact with the Amazon Web Services. // // https://wiki.ubuntu.com/goamz // // Copyright (c) 2011 Canonical Ltd. // // Written by Gustavo Niemeyer // package aws import ( "encoding/json" "encoding/xml" "errors" "fmt" "io/ioutil" "net" "net/http" "net/url" "os" "os/user" "path" "regexp" "strings" "time" ) // Regular expressions for INI files var ( iniSectionRegexp = regexp.MustCompile(`^\s*\[([^\[\]]+)\]\s*$`) iniSettingRegexp = regexp.MustCompile(`^\s*(.+?)\s*=\s*(.*\S)\s*$`) ) // Defines the valid signers const ( V2Signature = iota V4Signature = iota Route53Signature = iota ) // Defines the service endpoint and correct Signer implementation to use // to sign requests for this endpoint type ServiceInfo struct { Endpoint string Signer uint } // Region defines the URLs where AWS services may be accessed. // // See http://goo.gl/d8BP1 for more details. type Region struct { Name string // the canonical name of this region. EC2Endpoint ServiceInfo S3Endpoint string S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name. S3LocationConstraint bool // true if this region requires a LocationConstraint declaration. S3LowercaseBucket bool // true if the region requires bucket names to be lower case. SDBEndpoint string SNSEndpoint string SQSEndpoint string SESEndpoint string IAMEndpoint string ELBEndpoint string KMSEndpoint string DynamoDBEndpoint string CloudWatchServicepoint ServiceInfo AutoScalingEndpoint string RDSEndpoint ServiceInfo KinesisEndpoint string STSEndpoint string CloudFormationEndpoint string ElastiCacheEndpoint string } var Regions = map[string]Region{ APNortheast.Name: APNortheast, APNortheast2.Name: APNortheast2, APSoutheast.Name: APSoutheast, APSoutheast2.Name: APSoutheast2, EUCentral.Name: EUCentral, EUWest.Name: EUWest, USEast.Name: USEast, USWest.Name: USWest, USWest2.Name: USWest2, USGovWest.Name: USGovWest, SAEast.Name: SAEast, CNNorth1.Name: CNNorth1, } // Designates a signer interface suitable for signing AWS requests, params // should be appropriately encoded for the request before signing. // // A signer should be initialized with Auth and the appropriate endpoint. type Signer interface { Sign(method, path string, params map[string]string) } // An AWS Service interface with the API to query the AWS service // // Supplied as an easy way to mock out service calls during testing. type AWSService interface { // Queries the AWS service at a given method/path with the params and // returns an http.Response and error Query(method, path string, params map[string]string) (*http.Response, error) // Builds an error given an XML payload in the http.Response, can be used // to process an error if the status code is not 200 for example. BuildError(r *http.Response) error } // Implements a Server Query/Post API to easily query AWS services and build // errors when desired type Service struct { service ServiceInfo signer Signer } // Create a base set of params for an action func MakeParams(action string) map[string]string { params := make(map[string]string) params["Action"] = action return params } // Create a new AWS server to handle making requests func NewService(auth Auth, service ServiceInfo) (s *Service, err error) { var signer Signer switch service.Signer { case V2Signature: signer, err = NewV2Signer(auth, service) // case V4Signature: // signer, err = NewV4Signer(auth, service, Regions["eu-west-1"]) default: err = fmt.Errorf("Unsupported signer for service") } if err != nil { return } s = &Service{service: service, signer: signer} return } func (s *Service) Query(method, path string, params map[string]string) (resp *http.Response, err error) { params["Timestamp"] = time.Now().UTC().Format(time.RFC3339) u, err := url.Parse(s.service.Endpoint) if err != nil { return nil, err } u.Path = path s.signer.Sign(method, path, params) if method == "GET" { u.RawQuery = multimap(params).Encode() resp, err = http.Get(u.String()) } else if method == "POST" { resp, err = http.PostForm(u.String(), multimap(params)) } return } func (s *Service) BuildError(r *http.Response) error { errors := ErrorResponse{} xml.NewDecoder(r.Body).Decode(&errors) var err Error err = errors.Errors err.RequestId = errors.RequestId err.StatusCode = r.StatusCode if err.Message == "" { err.Message = r.Status } return &err } type ServiceError interface { error ErrorCode() string } type ErrorResponse struct { Errors Error `xml:"Error"` RequestId string // A unique ID for tracking the request } type Error struct { StatusCode int Type string Code string Message string RequestId string } func (err *Error) Error() string { return fmt.Sprintf("Type: %s, Code: %s, Message: %s", err.Type, err.Code, err.Message, ) } func (err *Error) ErrorCode() string { return err.Code } type Auth struct { AccessKey, SecretKey string token string expiration time.Time } func (a *Auth) Token() string { if a.token == "" { return "" } if time.Since(a.expiration) >= -30*time.Second { //in an ideal world this should be zero assuming the instance is synching it's clock auth, err := GetAuth("", "", "", time.Time{}) if err == nil { *a = auth } } return a.token } func (a *Auth) Expiration() time.Time { return a.expiration } // To be used with other APIs that return auth credentials such as STS func NewAuth(accessKey, secretKey, token string, expiration time.Time) *Auth { return &Auth{ AccessKey: accessKey, SecretKey: secretKey, token: token, expiration: expiration, } } // ResponseMetadata type ResponseMetadata struct { RequestId string // A unique ID for tracking the request } type BaseResponse struct { ResponseMetadata ResponseMetadata } var unreserved = make([]bool, 128) var hex = "0123456789ABCDEF" func init() { // RFC3986 u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~" for _, c := range u { unreserved[c] = true } } func multimap(p map[string]string) url.Values { q := make(url.Values, len(p)) for k, v := range p { q[k] = []string{v} } return q } type credentials struct { Code string LastUpdated string Type string AccessKeyId string SecretAccessKey string Token string Expiration string } // GetMetaData retrieves instance metadata about the current machine. // // See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details. func GetMetaData(path string) (contents []byte, err error) { c := http.Client{ Transport: &http.Transport{ Dial: func(netw, addr string) (net.Conn, error) { deadline := time.Now().Add(5 * time.Second) c, err := net.DialTimeout(netw, addr, time.Second*2) if err != nil { return nil, err } c.SetDeadline(deadline) return c, nil }, }, } url := "http://169.254.169.254/latest/meta-data/" + path resp, err := c.Get(url) if err != nil { return } defer resp.Body.Close() if resp.StatusCode != 200 { err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { return } return []byte(body), err } func GetRegion(regionName string) (region Region) { region = Regions[regionName] return } // GetInstanceCredentials creates an Auth based on the instance's role credentials. // If the running instance is not in EC2 or does not have a valid IAM role, an error will be returned. // For more info about setting up IAM roles, see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html func GetInstanceCredentials() (cred credentials, err error) { credentialPath := "iam/security-credentials/" // Get the instance role role, err := GetMetaData(credentialPath) if err != nil { return } // Get the instance role credentials credentialJSON, err := GetMetaData(credentialPath + string(role)) if err != nil { return } err = json.Unmarshal([]byte(credentialJSON), &cred) return } // GetAuth creates an Auth based on either passed in credentials, // environment information or instance based role credentials. func GetAuth(accessKey string, secretKey, token string, expiration time.Time) (auth Auth, err error) { // First try passed in credentials if accessKey != "" && secretKey != "" { return Auth{accessKey, secretKey, token, expiration}, nil } // Next try to get auth from the environment auth, err = EnvAuth() if err == nil { // Found auth, return return } // Next try getting auth from the instance role cred, err := GetInstanceCredentials() if err == nil { // Found auth, return auth.AccessKey = cred.AccessKeyId auth.SecretKey = cred.SecretAccessKey auth.token = cred.Token exptdate, err := time.Parse("2006-01-02T15:04:05Z", cred.Expiration) if err != nil { err = fmt.Errorf("Error Parsing expiration date: cred.Expiration :%s , error: %s \n", cred.Expiration, err) } auth.expiration = exptdate return auth, err } // Next try getting auth from the credentials file auth, err = CredentialFileAuth("", "", time.Minute*5) if err == nil { return } //err = errors.New("No valid AWS authentication found") err = fmt.Errorf("No valid AWS authentication found: %s", err) return auth, err } // EnvAuth creates an Auth based on environment information. // The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment // variables are used. func EnvAuth() (auth Auth, err error) { auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID") if auth.AccessKey == "" { auth.AccessKey = os.Getenv("AWS_ACCESS_KEY") } auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY") if auth.SecretKey == "" { auth.SecretKey = os.Getenv("AWS_SECRET_KEY") } if auth.AccessKey == "" { err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment") } if auth.SecretKey == "" { err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment") } return } // CredentialFileAuth creates and Auth based on a credentials file. The file // contains various authentication profiles for use with AWS. // // The credentials file, which is used by other AWS SDKs, is documented at // http://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs func CredentialFileAuth(filePath string, profile string, expiration time.Duration) (auth Auth, err error) { if profile == "" { profile = os.Getenv("AWS_DEFAULT_PROFILE") if profile == "" { profile = os.Getenv("AWS_PROFILE") if profile == "" { profile = "default" } } } if filePath == "" { u, err := user.Current() if err != nil { return auth, err } filePath = path.Join(u.HomeDir, ".aws", "credentials") } // read the file, then parse the INI contents, err := ioutil.ReadFile(filePath) if err != nil { return } profiles := parseINI(string(contents)) profileData, ok := profiles[profile] if !ok { err = errors.New("The credentials file did not contain the profile") return } keyId, ok := profileData["aws_access_key_id"] if !ok { err = errors.New("The credentials file did not contain required attribute aws_access_key_id") return } secretKey, ok := profileData["aws_secret_access_key"] if !ok { err = errors.New("The credentials file did not contain required attribute aws_secret_access_key") return } auth.AccessKey = keyId auth.SecretKey = secretKey if token, ok := profileData["aws_session_token"]; ok { auth.token = token } auth.expiration = time.Now().Add(expiration) return } // parseINI takes the contents of a credentials file and returns a map, whose keys // are the various profiles, and whose values are maps of the settings for the // profiles func parseINI(fileContents string) map[string]map[string]string { profiles := make(map[string]map[string]string) lines := strings.Split(fileContents, "\n") var currentSection map[string]string for _, line := range lines { // remove comments, which start with a semi-colon if split := strings.Split(line, ";"); len(split) > 1 { line = split[0] } // check if the line is the start of a profile. // // for example: // [default] // // otherwise, check for the proper setting // property=value if sectMatch := iniSectionRegexp.FindStringSubmatch(line); len(sectMatch) == 2 { currentSection = make(map[string]string) profiles[sectMatch[1]] = currentSection } else if setMatch := iniSettingRegexp.FindStringSubmatch(line); len(setMatch) == 3 && currentSection != nil { currentSection[setMatch[1]] = setMatch[2] } } return profiles } // Encode takes a string and URI-encodes it in a way suitable // to be used in AWS signatures. func Encode(s string) string { encode := false for i := 0; i != len(s); i++ { c := s[i] if c > 127 || !unreserved[c] { encode = true break } } if !encode { return s } e := make([]byte, len(s)*3) ei := 0 for i := 0; i != len(s); i++ { c := s[i] if c > 127 || !unreserved[c] { e[ei] = '%' e[ei+1] = hex[c>>4] e[ei+2] = hex[c&0xF] ei += 3 } else { e[ei] = c ei += 1 } } return string(e[:ei]) } func dialTimeout(network, addr string) (net.Conn, error) { return net.DialTimeout(network, addr, time.Duration(2*time.Second)) } func AvailabilityZone() string { transport := http.Transport{Dial: dialTimeout} client := http.Client{ Transport: &transport, } resp, err := client.Get("http://169.254.169.254/latest/meta-data/placement/availability-zone") if err != nil { return "unknown" } else { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "unknown" } else { return string(body) } } } func InstanceRegion() string { az := AvailabilityZone() if az == "unknown" { return az } else { region := az[:len(az)-1] return region } } func InstanceId() string { transport := http.Transport{Dial: dialTimeout} client := http.Client{ Transport: &transport, } resp, err := client.Get("http://169.254.169.254/latest/meta-data/instance-id") if err != nil { return "unknown" } else { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "unknown" } else { return string(body) } } } func InstanceType() string { transport := http.Transport{Dial: dialTimeout} client := http.Client{ Transport: &transport, } resp, err := client.Get("http://169.254.169.254/latest/meta-data/instance-type") if err != nil { return "unknown" } else { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "unknown" } else { return string(body) } } } func ServerLocalIp() string { transport := http.Transport{Dial: dialTimeout} client := http.Client{ Transport: &transport, } resp, err := client.Get("http://169.254.169.254/latest/meta-data/local-ipv4") if err != nil { return "127.0.0.1" } else { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "127.0.0.1" } else { return string(body) } } } func ServerPublicIp() string { transport := http.Transport{Dial: dialTimeout} client := http.Client{ Transport: &transport, } resp, err := client.Get("http://169.254.169.254/latest/meta-data/public-ipv4") if err != nil { return "127.0.0.1" } else { defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return "127.0.0.1" } else { return string(body) } } } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/client.go000066400000000000000000000057251313450123100257650ustar00rootroot00000000000000package aws import ( "math" "net" "net/http" "time" ) type RetryableFunc func(*http.Request, *http.Response, error) bool type WaitFunc func(try int) type DeadlineFunc func() time.Time type ResilientTransport struct { // Timeout is the maximum amount of time a dial will wait for // a connect to complete. // // The default is no timeout. // // With or without a timeout, the operating system may impose // its own earlier timeout. For instance, TCP timeouts are // often around 3 minutes. DialTimeout time.Duration // MaxTries, if non-zero, specifies the number of times we will retry on // failure. Retries are only attempted for temporary network errors or known // safe failures. MaxTries int Deadline DeadlineFunc ShouldRetry RetryableFunc Wait WaitFunc transport *http.Transport } // Convenience method for creating an http client func NewClient(rt *ResilientTransport) *http.Client { rt.transport = &http.Transport{ Dial: func(netw, addr string) (net.Conn, error) { c, err := net.DialTimeout(netw, addr, rt.DialTimeout) if err != nil { return nil, err } c.SetDeadline(rt.Deadline()) return c, nil }, Proxy: http.ProxyFromEnvironment, } // TODO: Would be nice is ResilientTransport allowed clients to initialize // with http.Transport attributes. return &http.Client{ Transport: rt, } } var retryingTransport = &ResilientTransport{ Deadline: func() time.Time { return time.Now().Add(5 * time.Second) }, DialTimeout: 10 * time.Second, MaxTries: 3, ShouldRetry: awsRetry, Wait: ExpBackoff, } // Exported default client var RetryingClient = NewClient(retryingTransport) func (t *ResilientTransport) RoundTrip(req *http.Request) (*http.Response, error) { return t.tries(req) } // Retry a request a maximum of t.MaxTries times. // We'll only retry if the proper criteria are met. // If a wait function is specified, wait that amount of time // In between requests. func (t *ResilientTransport) tries(req *http.Request) (res *http.Response, err error) { for try := 0; try < t.MaxTries; try += 1 { res, err = t.transport.RoundTrip(req) if !t.ShouldRetry(req, res, err) { break } if res != nil { res.Body.Close() } if t.Wait != nil { t.Wait(try) } } return } func ExpBackoff(try int) { time.Sleep(100 * time.Millisecond * time.Duration(math.Exp2(float64(try)))) } func LinearBackoff(try int) { time.Sleep(time.Duration(try*100) * time.Millisecond) } // Decide if we should retry a request. // In general, the criteria for retrying a request is described here // http://docs.aws.amazon.com/general/latest/gr/api-retries.html func awsRetry(req *http.Request, res *http.Response, err error) bool { retry := false // Retry if there's a temporary network error. if neterr, ok := err.(net.Error); ok { if neterr.Temporary() { retry = true } } // Retry if we get a 5xx series error. if res != nil { if res.StatusCode >= 500 && res.StatusCode < 600 { retry = true } } return retry } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/regions.go000066400000000000000000000233751313450123100261560ustar00rootroot00000000000000package aws var USGovWest = Region{ "us-gov-west-1", ServiceInfo{"https://ec2.us-gov-west-1.amazonaws.com", V2Signature}, "https://s3-fips-us-gov-west-1.amazonaws.com", "", true, true, "", "https://sns.us-gov-west-1.amazonaws.com", "https://sqs.us-gov-west-1.amazonaws.com", "", "https://iam.us-gov.amazonaws.com", "https://elasticloadbalancing.us-gov-west-1.amazonaws.com", "", "https://dynamodb.us-gov-west-1.amazonaws.com", ServiceInfo{"https://monitoring.us-gov-west-1.amazonaws.com", V2Signature}, "https://autoscaling.us-gov-west-1.amazonaws.com", ServiceInfo{"https://rds.us-gov-west-1.amazonaws.com", V2Signature}, "", "https://sts.amazonaws.com", "https://cloudformation.us-gov-west-1.amazonaws.com", "", } var USEast = Region{ "us-east-1", ServiceInfo{"https://ec2.us-east-1.amazonaws.com", V2Signature}, "https://s3-external-1.amazonaws.com", "", false, false, "https://sdb.amazonaws.com", "https://sns.us-east-1.amazonaws.com", "https://sqs.us-east-1.amazonaws.com", "https://email.us-east-1.amazonaws.com", "https://iam.amazonaws.com", "https://elasticloadbalancing.us-east-1.amazonaws.com", "https://kms.us-east-1.amazonaws.com", "https://dynamodb.us-east-1.amazonaws.com", ServiceInfo{"https://monitoring.us-east-1.amazonaws.com", V2Signature}, "https://autoscaling.us-east-1.amazonaws.com", ServiceInfo{"https://rds.us-east-1.amazonaws.com", V2Signature}, "https://kinesis.us-east-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.us-east-1.amazonaws.com", "https://elasticache.us-east-1.amazonaws.com", } var USWest = Region{ "us-west-1", ServiceInfo{"https://ec2.us-west-1.amazonaws.com", V2Signature}, "https://s3-us-west-1.amazonaws.com", "", true, true, "https://sdb.us-west-1.amazonaws.com", "https://sns.us-west-1.amazonaws.com", "https://sqs.us-west-1.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.us-west-1.amazonaws.com", "https://kms.us-west-1.amazonaws.com", "https://dynamodb.us-west-1.amazonaws.com", ServiceInfo{"https://monitoring.us-west-1.amazonaws.com", V2Signature}, "https://autoscaling.us-west-1.amazonaws.com", ServiceInfo{"https://rds.us-west-1.amazonaws.com", V2Signature}, "https://kinesis.us-west-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.us-west-1.amazonaws.com", "https://elasticache.us-west-1.amazonaws.com", } var USWest2 = Region{ "us-west-2", ServiceInfo{"https://ec2.us-west-2.amazonaws.com", V2Signature}, "https://s3-us-west-2.amazonaws.com", "", true, true, "https://sdb.us-west-2.amazonaws.com", "https://sns.us-west-2.amazonaws.com", "https://sqs.us-west-2.amazonaws.com", "https://email.us-west-2.amazonaws.com", "https://iam.amazonaws.com", "https://elasticloadbalancing.us-west-2.amazonaws.com", "https://kms.us-west-2.amazonaws.com", "https://dynamodb.us-west-2.amazonaws.com", ServiceInfo{"https://monitoring.us-west-2.amazonaws.com", V2Signature}, "https://autoscaling.us-west-2.amazonaws.com", ServiceInfo{"https://rds.us-west-2.amazonaws.com", V2Signature}, "https://kinesis.us-west-2.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.us-west-2.amazonaws.com", "https://elasticache.us-west-2.amazonaws.com", } var EUWest = Region{ "eu-west-1", ServiceInfo{"https://ec2.eu-west-1.amazonaws.com", V2Signature}, "https://s3-eu-west-1.amazonaws.com", "", true, true, "https://sdb.eu-west-1.amazonaws.com", "https://sns.eu-west-1.amazonaws.com", "https://sqs.eu-west-1.amazonaws.com", "https://email.eu-west-1.amazonaws.com", "https://iam.amazonaws.com", "https://elasticloadbalancing.eu-west-1.amazonaws.com", "https://kms.eu-west-1.amazonaws.com", "https://dynamodb.eu-west-1.amazonaws.com", ServiceInfo{"https://monitoring.eu-west-1.amazonaws.com", V2Signature}, "https://autoscaling.eu-west-1.amazonaws.com", ServiceInfo{"https://rds.eu-west-1.amazonaws.com", V2Signature}, "https://kinesis.eu-west-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.eu-west-1.amazonaws.com", "https://elasticache.eu-west-1.amazonaws.com", } var EUCentral = Region{ "eu-central-1", ServiceInfo{"https://ec2.eu-central-1.amazonaws.com", V4Signature}, "https://s3-eu-central-1.amazonaws.com", "", true, true, "https://sdb.eu-central-1.amazonaws.com", "https://sns.eu-central-1.amazonaws.com", "https://sqs.eu-central-1.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.eu-central-1.amazonaws.com", "https://kms.eu-central-1.amazonaws.com", "https://dynamodb.eu-central-1.amazonaws.com", ServiceInfo{"https://monitoring.eu-central-1.amazonaws.com", V2Signature}, "https://autoscaling.eu-central-1.amazonaws.com", ServiceInfo{"https://rds.eu-central-1.amazonaws.com", V2Signature}, "https://kinesis.eu-central-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.eu-central-1.amazonaws.com", "", } var APSoutheast = Region{ "ap-southeast-1", ServiceInfo{"https://ec2.ap-southeast-1.amazonaws.com", V2Signature}, "https://s3-ap-southeast-1.amazonaws.com", "", true, true, "https://sdb.ap-southeast-1.amazonaws.com", "https://sns.ap-southeast-1.amazonaws.com", "https://sqs.ap-southeast-1.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.ap-southeast-1.amazonaws.com", "https://kms.ap-southeast-1.amazonaws.com", "https://dynamodb.ap-southeast-1.amazonaws.com", ServiceInfo{"https://monitoring.ap-southeast-1.amazonaws.com", V2Signature}, "https://autoscaling.ap-southeast-1.amazonaws.com", ServiceInfo{"https://rds.ap-southeast-1.amazonaws.com", V2Signature}, "https://kinesis.ap-southeast-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.ap-southeast-1.amazonaws.com", "https://elasticache.ap-southeast-1.amazonaws.com", } var APSoutheast2 = Region{ "ap-southeast-2", ServiceInfo{"https://ec2.ap-southeast-2.amazonaws.com", V2Signature}, "https://s3-ap-southeast-2.amazonaws.com", "", true, true, "https://sdb.ap-southeast-2.amazonaws.com", "https://sns.ap-southeast-2.amazonaws.com", "https://sqs.ap-southeast-2.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.ap-southeast-2.amazonaws.com", "https://kms.ap-southeast-2.amazonaws.com", "https://dynamodb.ap-southeast-2.amazonaws.com", ServiceInfo{"https://monitoring.ap-southeast-2.amazonaws.com", V2Signature}, "https://autoscaling.ap-southeast-2.amazonaws.com", ServiceInfo{"https://rds.ap-southeast-2.amazonaws.com", V2Signature}, "https://kinesis.ap-southeast-2.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.ap-southeast-2.amazonaws.com", "https://elasticache.ap-southeast-2.amazonaws.com", } var APNortheast = Region{ "ap-northeast-1", ServiceInfo{"https://ec2.ap-northeast-1.amazonaws.com", V2Signature}, "https://s3-ap-northeast-1.amazonaws.com", "", true, true, "https://sdb.ap-northeast-1.amazonaws.com", "https://sns.ap-northeast-1.amazonaws.com", "https://sqs.ap-northeast-1.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.ap-northeast-1.amazonaws.com", "https://kms.ap-northeast-1.amazonaws.com", "https://dynamodb.ap-northeast-1.amazonaws.com", ServiceInfo{"https://monitoring.ap-northeast-1.amazonaws.com", V2Signature}, "https://autoscaling.ap-northeast-1.amazonaws.com", ServiceInfo{"https://rds.ap-northeast-1.amazonaws.com", V2Signature}, "https://kinesis.ap-northeast-1.amazonaws.com", "https://sts.amazonaws.com", "https://cloudformation.ap-northeast-1.amazonaws.com", "https://elasticache.ap-northeast-1.amazonaws.com", } var APNortheast2 = Region{ "ap-northeast-2", ServiceInfo{"https://ec2.ap-northeast-2.amazonaws.com", V2Signature}, "https://s3-ap-northeast-2.amazonaws.com", "", true, true, "", "https://sns.ap-northeast-2.amazonaws.com", "https://sqs.ap-northeast-2.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.ap-northeast-2.amazonaws.com", "https://kms.ap-northeast-2.amazonaws.com", "https://dynamodb.ap-northeast-2.amazonaws.com", ServiceInfo{"https://monitoring.ap-northeast-2.amazonaws.com", V2Signature}, "https://autoscaling.ap-northeast-2.amazonaws.com", ServiceInfo{"https://rds.ap-northeast-2.amazonaws.com", V2Signature}, "https://kinesis.ap-northeast-2.amazonaws.com", "https://sts.ap-northeast-2.amazonaws.com", "https://cloudformation.ap-northeast-2.amazonaws.com", "https://elasticache.ap-northeast-2.amazonaws.com", } var SAEast = Region{ "sa-east-1", ServiceInfo{"https://ec2.sa-east-1.amazonaws.com", V2Signature}, "https://s3-sa-east-1.amazonaws.com", "", true, true, "https://sdb.sa-east-1.amazonaws.com", "https://sns.sa-east-1.amazonaws.com", "https://sqs.sa-east-1.amazonaws.com", "", "https://iam.amazonaws.com", "https://elasticloadbalancing.sa-east-1.amazonaws.com", "https://kms.sa-east-1.amazonaws.com", "https://dynamodb.sa-east-1.amazonaws.com", ServiceInfo{"https://monitoring.sa-east-1.amazonaws.com", V2Signature}, "https://autoscaling.sa-east-1.amazonaws.com", ServiceInfo{"https://rds.sa-east-1.amazonaws.com", V2Signature}, "", "https://sts.amazonaws.com", "https://cloudformation.sa-east-1.amazonaws.com", "https://elasticache.sa-east-1.amazonaws.com", } var CNNorth1 = Region{ "cn-north-1", ServiceInfo{"https://ec2.cn-north-1.amazonaws.com.cn", V2Signature}, "https://s3.cn-north-1.amazonaws.com.cn", "", true, true, "", "https://sns.cn-north-1.amazonaws.com.cn", "https://sqs.cn-north-1.amazonaws.com.cn", "", "https://iam.cn-north-1.amazonaws.com.cn", "https://elasticloadbalancing.cn-north-1.amazonaws.com.cn", "", "https://dynamodb.cn-north-1.amazonaws.com.cn", ServiceInfo{"https://monitoring.cn-north-1.amazonaws.com.cn", V4Signature}, "https://autoscaling.cn-north-1.amazonaws.com.cn", ServiceInfo{"https://rds.cn-north-1.amazonaws.com.cn", V4Signature}, "", "https://sts.cn-north-1.amazonaws.com.cn", "", "", } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/retry.go000066400000000000000000000105411313450123100256440ustar00rootroot00000000000000package aws import ( "math/rand" "net" "net/http" "time" ) const ( maxDelay = 20 * time.Second defaultScale = 300 * time.Millisecond throttlingScale = 500 * time.Millisecond throttlingScaleRange = throttlingScale / 4 defaultMaxRetries = 3 dynamoDBScale = 25 * time.Millisecond dynamoDBMaxRetries = 10 ) // A RetryPolicy encapsulates a strategy for implementing client retries. // // Default implementations are provided which match the AWS SDKs. type RetryPolicy interface { // ShouldRetry returns whether a client should retry a failed request. ShouldRetry(target string, r *http.Response, err error, numRetries int) bool // Delay returns the time a client should wait before issuing a retry. Delay(target string, r *http.Response, err error, numRetries int) time.Duration } // DefaultRetryPolicy implements the AWS SDK default retry policy. // // It will retry up to 3 times, and uses an exponential backoff with a scale // factor of 300ms (300ms, 600ms, 1200ms). If the retry is because of // throttling, the delay will also include some randomness. // // See https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/PredefinedRetryPolicies.java#L90. type DefaultRetryPolicy struct { } // ShouldRetry implements the RetryPolicy ShouldRetry method. func (policy DefaultRetryPolicy) ShouldRetry(target string, r *http.Response, err error, numRetries int) bool { return shouldRetry(r, err, numRetries, defaultMaxRetries) } // Delay implements the RetryPolicy Delay method. func (policy DefaultRetryPolicy) Delay(target string, r *http.Response, err error, numRetries int) time.Duration { scale := defaultScale if err, ok := err.(*Error); ok && isThrottlingException(err) { scale = throttlingScale + time.Duration(rand.Int63n(int64(throttlingScaleRange))) } return exponentialBackoff(numRetries, scale) } // DynamoDBRetryPolicy implements the AWS SDK DynamoDB retry policy. // // It will retry up to 10 times, and uses an exponential backoff with a scale // factor of 25ms (25ms, 50ms, 100ms, ...). // // See https://github.com/aws/aws-sdk-java/blob/master/aws-java-sdk-core/src/main/java/com/amazonaws/retry/PredefinedRetryPolicies.java#L103. type DynamoDBRetryPolicy struct { } // ShouldRetry implements the RetryPolicy ShouldRetry method. func (policy DynamoDBRetryPolicy) ShouldRetry(target string, r *http.Response, err error, numRetries int) bool { return shouldRetry(r, err, numRetries, dynamoDBMaxRetries) } // Delay implements the RetryPolicy Delay method. func (policy DynamoDBRetryPolicy) Delay(target string, r *http.Response, err error, numRetries int) time.Duration { return exponentialBackoff(numRetries, dynamoDBScale) } // NeverRetryPolicy never retries requests and returns immediately on failure. type NeverRetryPolicy struct { } // ShouldRetry implements the RetryPolicy ShouldRetry method. func (policy NeverRetryPolicy) ShouldRetry(target string, r *http.Response, err error, numRetries int) bool { return false } // Delay implements the RetryPolicy Delay method. func (policy NeverRetryPolicy) Delay(target string, r *http.Response, err error, numRetries int) time.Duration { return time.Duration(0) } // shouldRetry determines if we should retry the request. // // See http://docs.aws.amazon.com/general/latest/gr/api-retries.html. func shouldRetry(r *http.Response, err error, numRetries int, maxRetries int) bool { // Once we've exceeded the max retry attempts, game over. if numRetries >= maxRetries { return false } // Always retry temporary network errors. if err, ok := err.(net.Error); ok && err.Temporary() { return true } // Always retry 5xx responses. if r != nil && r.StatusCode >= 500 { return true } // Always retry throttling exceptions. if err, ok := err.(ServiceError); ok && isThrottlingException(err) { return true } // Other classes of failures indicate a problem with the request. Retrying // won't help. return false } func exponentialBackoff(numRetries int, scale time.Duration) time.Duration { if numRetries < 0 { return time.Duration(0) } delay := (1 << uint(numRetries)) * scale if delay > maxDelay { return maxDelay } return delay } func isThrottlingException(err ServiceError) bool { switch err.ErrorCode() { case "Throttling", "ThrottlingException", "ProvisionedThroughputExceededException": return true default: return false } } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/aws/sign.go000066400000000000000000000330401313450123100254360ustar00rootroot00000000000000package aws import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "io/ioutil" "net/http" "net/url" "path" "sort" "strings" "time" ) // AWS specifies that the parameters in a signed request must // be provided in the natural order of the keys. This is distinct // from the natural order of the encoded value of key=value. // Percent and gocheck.Equals affect the sorting order. func EncodeSorted(values url.Values) string { // preallocate the arrays for perfomance keys := make([]string, 0, len(values)) sarray := make([]string, 0, len(values)) for k := range values { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { for _, v := range values[k] { sarray = append(sarray, Encode(k)+"="+Encode(v)) } } return strings.Join(sarray, "&") } type V2Signer struct { auth Auth service ServiceInfo host string } var b64 = base64.StdEncoding func NewV2Signer(auth Auth, service ServiceInfo) (*V2Signer, error) { u, err := url.Parse(service.Endpoint) if err != nil { return nil, err } return &V2Signer{auth: auth, service: service, host: u.Host}, nil } func (s *V2Signer) Sign(method, path string, params map[string]string) { params["AWSAccessKeyId"] = s.auth.AccessKey params["SignatureVersion"] = "2" params["SignatureMethod"] = "HmacSHA256" if s.auth.Token() != "" { params["SecurityToken"] = s.auth.Token() } // AWS specifies that the parameters in a signed request must // be provided in the natural order of the keys. This is distinct // from the natural order of the encoded value of key=value. // Percent and gocheck.Equals affect the sorting order. var keys, sarray []string for k := range params { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { sarray = append(sarray, Encode(k)+"="+Encode(params[k])) } joined := strings.Join(sarray, "&") payload := method + "\n" + s.host + "\n" + path + "\n" + joined hash := hmac.New(sha256.New, []byte(s.auth.SecretKey)) hash.Write([]byte(payload)) signature := make([]byte, b64.EncodedLen(hash.Size())) b64.Encode(signature, hash.Sum(nil)) params["Signature"] = string(signature) } func (s *V2Signer) SignRequest(req *http.Request) error { req.ParseForm() req.Form.Set("AWSAccessKeyId", s.auth.AccessKey) req.Form.Set("SignatureVersion", "2") req.Form.Set("SignatureMethod", "HmacSHA256") if s.auth.Token() != "" { req.Form.Set("SecurityToken", s.auth.Token()) } payload := req.Method + "\n" + req.URL.Host + "\n" + req.URL.Path + "\n" + EncodeSorted(req.Form) hash := hmac.New(sha256.New, []byte(s.auth.SecretKey)) hash.Write([]byte(payload)) signature := make([]byte, b64.EncodedLen(hash.Size())) b64.Encode(signature, hash.Sum(nil)) req.Form.Set("Signature", string(signature)) req.URL.RawQuery = req.Form.Encode() return nil } // Common date formats for signing requests const ( ISO8601BasicFormat = "20060102T150405Z" ISO8601BasicFormatShort = "20060102" ) type Route53Signer struct { auth Auth } func NewRoute53Signer(auth Auth) *Route53Signer { return &Route53Signer{auth: auth} } // Creates the authorize signature based on the date stamp and secret key func (s *Route53Signer) getHeaderAuthorize(message string) string { hmacSha256 := hmac.New(sha256.New, []byte(s.auth.SecretKey)) hmacSha256.Write([]byte(message)) cryptedString := hmacSha256.Sum(nil) return base64.StdEncoding.EncodeToString(cryptedString) } // Adds all the required headers for AWS Route53 API to the request // including the authorization func (s *Route53Signer) Sign(req *http.Request) { date := time.Now().UTC().Format(time.RFC1123) delete(req.Header, "Date") req.Header.Set("Date", date) authHeader := fmt.Sprintf("AWS3-HTTPS AWSAccessKeyId=%s,Algorithm=%s,Signature=%s", s.auth.AccessKey, "HmacSHA256", s.getHeaderAuthorize(date)) req.Header.Set("Host", req.Host) req.Header.Set("X-Amzn-Authorization", authHeader) req.Header.Set("Content-Type", "application/xml") if s.auth.Token() != "" { req.Header.Set("X-Amz-Security-Token", s.auth.Token()) } } /* The V4Signer encapsulates all of the functionality to sign a request with the AWS Signature Version 4 Signing Process. (http://goo.gl/u1OWZz) */ type V4Signer struct { auth Auth serviceName string region Region // Add the x-amz-content-sha256 header IncludeXAmzContentSha256 bool } /* Return a new instance of a V4Signer capable of signing AWS requests. */ func NewV4Signer(auth Auth, serviceName string, region Region) *V4Signer { return &V4Signer{ auth: auth, serviceName: serviceName, region: region, IncludeXAmzContentSha256: false, } } /* Sign a request according to the AWS Signature Version 4 Signing Process. (http://goo.gl/u1OWZz) The signed request will include an "x-amz-date" header with a current timestamp if a valid "x-amz-date" or "date" header was not available in the original request. In addition, AWS Signature Version 4 requires the "host" header to be a signed header, therefor the Sign method will manually set a "host" header from the request.Host. The signed request will include a new "Authorization" header indicating that the request has been signed. Any changes to the request after signing the request will invalidate the signature. */ func (s *V4Signer) Sign(req *http.Request) { req.Header.Set("host", req.Host) // host header must be included as a signed header t := s.requestTime(req) // Get request time payloadHash := "" if _, ok := req.Form["X-Amz-Expires"]; ok { // We are authenticating the the request by using query params // (also known as pre-signing a url, http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) payloadHash = "UNSIGNED-PAYLOAD" req.Header.Del("x-amz-date") req.Form["X-Amz-SignedHeaders"] = []string{s.signedHeaders(req.Header)} req.Form["X-Amz-Algorithm"] = []string{"AWS4-HMAC-SHA256"} req.Form["X-Amz-Credential"] = []string{s.auth.AccessKey + "/" + s.credentialScope(t)} req.Form["X-Amz-Date"] = []string{t.Format(ISO8601BasicFormat)} req.URL.RawQuery = req.Form.Encode() } else { payloadHash = s.payloadHash(req) if s.IncludeXAmzContentSha256 { req.Header.Set("x-amz-content-sha256", payloadHash) // x-amz-content-sha256 contains the payload hash } } creq := s.canonicalRequest(req, payloadHash) // Build canonical request sts := s.stringToSign(t, creq) // Build string to sign signature := s.signature(t, sts) // Calculate the AWS Signature Version 4 auth := s.authorization(req.Header, t, signature) // Create Authorization header value if _, ok := req.Form["X-Amz-Expires"]; ok { req.Form["X-Amz-Signature"] = []string{signature} } else { req.Header.Set("Authorization", auth) // Add Authorization header to request } return } func (s *V4Signer) SignRequest(req *http.Request) error { s.Sign(req) return nil } /* requestTime method will parse the time from the request "x-amz-date" or "date" headers. If the "x-amz-date" header is present, that will take priority over the "date" header. If neither header is defined or we are unable to parse either header as a valid date then we will create a new "x-amz-date" header with the current time. */ func (s *V4Signer) requestTime(req *http.Request) time.Time { // Get "x-amz-date" header date := req.Header.Get("x-amz-date") // Attempt to parse as ISO8601BasicFormat t, err := time.Parse(ISO8601BasicFormat, date) if err == nil { return t } // Attempt to parse as http.TimeFormat t, err = time.Parse(http.TimeFormat, date) if err == nil { req.Header.Set("x-amz-date", t.Format(ISO8601BasicFormat)) return t } // Get "date" header date = req.Header.Get("date") // Attempt to parse as http.TimeFormat t, err = time.Parse(http.TimeFormat, date) if err == nil { return t } // Create a current time header to be used t = time.Now().UTC() req.Header.Set("x-amz-date", t.Format(ISO8601BasicFormat)) return t } /* canonicalRequest method creates the canonical request according to Task 1 of the AWS Signature Version 4 Signing Process. (http://goo.gl/eUUZ3S) CanonicalRequest = HTTPRequestMethod + '\n' + CanonicalURI + '\n' + CanonicalQueryString + '\n' + CanonicalHeaders + '\n' + SignedHeaders + '\n' + HexEncode(Hash(Payload)) payloadHash is optional; use the empty string and it will be calculated from the request */ func (s *V4Signer) canonicalRequest(req *http.Request, payloadHash string) string { if payloadHash == "" { payloadHash = s.payloadHash(req) } c := new(bytes.Buffer) fmt.Fprintf(c, "%s\n", req.Method) fmt.Fprintf(c, "%s\n", s.canonicalURI(req.URL)) fmt.Fprintf(c, "%s\n", s.canonicalQueryString(req.URL)) fmt.Fprintf(c, "%s\n\n", s.canonicalHeaders(req.Header)) fmt.Fprintf(c, "%s\n", s.signedHeaders(req.Header)) fmt.Fprintf(c, "%s", payloadHash) return c.String() } func (s *V4Signer) canonicalURI(u *url.URL) string { u = &url.URL{Path: u.Path} canonicalPath := u.String() slash := strings.HasSuffix(canonicalPath, "/") canonicalPath = path.Clean(canonicalPath) if canonicalPath == "" || canonicalPath == "." { canonicalPath = "/" } if canonicalPath != "/" && slash { canonicalPath += "/" } return canonicalPath } func (s *V4Signer) canonicalQueryString(u *url.URL) string { keyValues := make(map[string]string, len(u.Query())) keys := make([]string, len(u.Query())) key_i := 0 for k, vs := range u.Query() { k = url.QueryEscape(k) a := make([]string, len(vs)) for idx, v := range vs { v = url.QueryEscape(v) a[idx] = fmt.Sprintf("%s=%s", k, v) } keyValues[k] = strings.Join(a, "&") keys[key_i] = k key_i++ } sort.Strings(keys) query := make([]string, len(keys)) for idx, key := range keys { query[idx] = keyValues[key] } query_str := strings.Join(query, "&") // AWS V4 signing requires that the space characters // are encoded as %20 instead of +. On the other hand // golangs url.QueryEscape as well as url.Values.Encode() // both encode the space as a + character. See: // http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html // https://github.com/golang/go/issues/4013 // https://groups.google.com/forum/#!topic/golang-nuts/BB443qEjPIk return strings.Replace(query_str, "+", "%20", -1) } func (s *V4Signer) canonicalHeaders(h http.Header) string { i, a, lowerCase := 0, make([]string, len(h)), make(map[string][]string) for k, v := range h { lowerCase[strings.ToLower(k)] = v } var keys []string for k := range lowerCase { keys = append(keys, k) } sort.Strings(keys) for _, k := range keys { v := lowerCase[k] for j, w := range v { v[j] = strings.Trim(w, " ") } sort.Strings(v) a[i] = strings.ToLower(k) + ":" + strings.Join(v, ",") i++ } return strings.Join(a, "\n") } func (s *V4Signer) signedHeaders(h http.Header) string { i, a := 0, make([]string, len(h)) for k := range h { a[i] = strings.ToLower(k) i++ } sort.Strings(a) return strings.Join(a, ";") } func (s *V4Signer) payloadHash(req *http.Request) string { var b []byte if req.Body == nil { b = []byte("") } else { var err error b, err = ioutil.ReadAll(req.Body) if err != nil { // TODO: I REALLY DON'T LIKE THIS PANIC!!!! panic(err) } } req.Body = ioutil.NopCloser(bytes.NewBuffer(b)) return s.hash(string(b)) } /* stringToSign method creates the string to sign accorting to Task 2 of the AWS Signature Version 4 Signing Process. (http://goo.gl/es1PAu) StringToSign = Algorithm + '\n' + RequestDate + '\n' + CredentialScope + '\n' + HexEncode(Hash(CanonicalRequest)) */ func (s *V4Signer) stringToSign(t time.Time, creq string) string { w := new(bytes.Buffer) fmt.Fprint(w, "AWS4-HMAC-SHA256\n") fmt.Fprintf(w, "%s\n", t.Format(ISO8601BasicFormat)) fmt.Fprintf(w, "%s\n", s.credentialScope(t)) fmt.Fprintf(w, "%s", s.hash(creq)) return w.String() } func (s *V4Signer) credentialScope(t time.Time) string { return fmt.Sprintf("%s/%s/%s/aws4_request", t.Format(ISO8601BasicFormatShort), s.region.Name, s.serviceName) } /* signature method calculates the AWS Signature Version 4 according to Task 3 of the AWS Signature Version 4 Signing Process. (http://goo.gl/j0Yqe1) signature = HexEncode(HMAC(derived-signing-key, string-to-sign)) */ func (s *V4Signer) signature(t time.Time, sts string) string { h := s.hmac(s.derivedKey(t), []byte(sts)) return fmt.Sprintf("%x", h) } /* derivedKey method derives a signing key to be used for signing a request. kSecret = Your AWS Secret Access Key kDate = HMAC("AWS4" + kSecret, Date) kRegion = HMAC(kDate, Region) kService = HMAC(kRegion, Service) kSigning = HMAC(kService, "aws4_request") */ func (s *V4Signer) derivedKey(t time.Time) []byte { h := s.hmac([]byte("AWS4"+s.auth.SecretKey), []byte(t.Format(ISO8601BasicFormatShort))) h = s.hmac(h, []byte(s.region.Name)) h = s.hmac(h, []byte(s.serviceName)) h = s.hmac(h, []byte("aws4_request")) return h } /* authorization method generates the authorization header value. */ func (s *V4Signer) authorization(header http.Header, t time.Time, signature string) string { w := new(bytes.Buffer) fmt.Fprint(w, "AWS4-HMAC-SHA256 ") fmt.Fprintf(w, "Credential=%s/%s, ", s.auth.AccessKey, s.credentialScope(t)) fmt.Fprintf(w, "SignedHeaders=%s, ", s.signedHeaders(header)) fmt.Fprintf(w, "Signature=%s", signature) return w.String() } // hash method calculates the sha256 hash for a given string func (s *V4Signer) hash(in string) string { h := sha256.New() fmt.Fprintf(h, "%s", in) return fmt.Sprintf("%x", h.Sum(nil)) } // hmac method calculates the sha256 hmac for a given slice of bytes func (s *V4Signer) hmac(key, data []byte) []byte { h := hmac.New(sha256.New, key) h.Write(data) return h.Sum(nil) } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/s3/000077500000000000000000000000001313450123100237025ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/s3/lifecycle.go000066400000000000000000000125701313450123100261750ustar00rootroot00000000000000package s3 import ( "crypto/md5" "encoding/base64" "encoding/xml" "net/url" "strconv" "time" ) // Implements an interface for s3 bucket lifecycle configuration // See goo.gl/d0bbDf for details. const ( LifecycleRuleStatusEnabled = "Enabled" LifecycleRuleStatusDisabled = "Disabled" LifecycleRuleDateFormat = "2006-01-02" StorageClassGlacier = "GLACIER" ) type Expiration struct { Days *uint `xml:"Days,omitempty"` Date string `xml:"Date,omitempty"` } // Returns Date as a time.Time. func (r *Expiration) ParseDate() (time.Time, error) { return time.Parse(LifecycleRuleDateFormat, r.Date) } type Transition struct { Days *uint `xml:"Days,omitempty"` Date string `xml:"Date,omitempty"` StorageClass string `xml:"StorageClass"` } // Returns Date as a time.Time. func (r *Transition) ParseDate() (time.Time, error) { return time.Parse(LifecycleRuleDateFormat, r.Date) } type NoncurrentVersionExpiration struct { Days *uint `xml:"NoncurrentDays,omitempty"` } type NoncurrentVersionTransition struct { Days *uint `xml:"NoncurrentDays,omitempty"` StorageClass string `xml:"StorageClass"` } type LifecycleRule struct { ID string `xml:"ID"` Prefix string `xml:"Prefix"` Status string `xml:"Status"` NoncurrentVersionTransition *NoncurrentVersionTransition `xml:"NoncurrentVersionTransition,omitempty"` NoncurrentVersionExpiration *NoncurrentVersionExpiration `xml:"NoncurrentVersionExpiration,omitempty"` Transition *Transition `xml:"Transition,omitempty"` Expiration *Expiration `xml:"Expiration,omitempty"` } // Create a lifecycle rule with arbitrary identifier id and object name prefix // for which the rules should apply. func NewLifecycleRule(id, prefix string) *LifecycleRule { rule := &LifecycleRule{ ID: id, Prefix: prefix, Status: LifecycleRuleStatusEnabled, } return rule } // Adds a transition rule in days. Overwrites any previous transition rule. func (r *LifecycleRule) SetTransitionDays(days uint) { r.Transition = &Transition{ Days: &days, StorageClass: StorageClassGlacier, } } // Adds a transition rule as a date. Overwrites any previous transition rule. func (r *LifecycleRule) SetTransitionDate(date time.Time) { r.Transition = &Transition{ Date: date.Format(LifecycleRuleDateFormat), StorageClass: StorageClassGlacier, } } // Adds an expiration rule in days. Overwrites any previous expiration rule. // Days must be > 0. func (r *LifecycleRule) SetExpirationDays(days uint) { r.Expiration = &Expiration{ Days: &days, } } // Adds an expiration rule as a date. Overwrites any previous expiration rule. func (r *LifecycleRule) SetExpirationDate(date time.Time) { r.Expiration = &Expiration{ Date: date.Format(LifecycleRuleDateFormat), } } // Adds a noncurrent version transition rule. Overwrites any previous // noncurrent version transition rule. func (r *LifecycleRule) SetNoncurrentVersionTransitionDays(days uint) { r.NoncurrentVersionTransition = &NoncurrentVersionTransition{ Days: &days, StorageClass: StorageClassGlacier, } } // Adds a noncurrent version expiration rule. Days must be > 0. Overwrites // any previous noncurrent version expiration rule. func (r *LifecycleRule) SetNoncurrentVersionExpirationDays(days uint) { r.NoncurrentVersionExpiration = &NoncurrentVersionExpiration{ Days: &days, } } // Marks the rule as disabled. func (r *LifecycleRule) Disable() { r.Status = LifecycleRuleStatusDisabled } // Marks the rule as enabled (default). func (r *LifecycleRule) Enable() { r.Status = LifecycleRuleStatusEnabled } type LifecycleConfiguration struct { XMLName xml.Name `xml:"LifecycleConfiguration"` Rules *[]*LifecycleRule `xml:"Rule,omitempty"` } // Adds a LifecycleRule to the configuration. func (c *LifecycleConfiguration) AddRule(r *LifecycleRule) { var rules []*LifecycleRule if c.Rules != nil { rules = *c.Rules } rules = append(rules, r) c.Rules = &rules } // Sets the bucket's lifecycle configuration. func (b *Bucket) PutLifecycleConfiguration(c *LifecycleConfiguration) error { doc, err := xml.Marshal(c) if err != nil { return err } buf := makeXmlBuffer(doc) digest := md5.New() size, err := digest.Write(buf.Bytes()) if err != nil { return err } headers := map[string][]string{ "Content-Length": {strconv.FormatInt(int64(size), 10)}, "Content-MD5": {base64.StdEncoding.EncodeToString(digest.Sum(nil))}, } req := &request{ path: "/", method: "PUT", bucket: b.Name, headers: headers, payload: buf, params: url.Values{"lifecycle": {""}}, } return b.S3.queryV4Sign(req, nil) } // Retrieves the lifecycle configuration for the bucket. AWS returns an error // if no lifecycle found. func (b *Bucket) GetLifecycleConfiguration() (*LifecycleConfiguration, error) { req := &request{ method: "GET", bucket: b.Name, path: "/", params: url.Values{"lifecycle": {""}}, } conf := &LifecycleConfiguration{} err := b.S3.queryV4Sign(req, conf) return conf, err } // Delete the bucket's lifecycle configuration. func (b *Bucket) DeleteLifecycleConfiguration() error { req := &request{ method: "DELETE", bucket: b.Name, path: "/", params: url.Values{"lifecycle": {""}}, } return b.S3.queryV4Sign(req, nil) } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/s3/multi.go000066400000000000000000000336041313450123100253710ustar00rootroot00000000000000package s3 import ( "bytes" "crypto/md5" "encoding/base64" "encoding/hex" "encoding/xml" "errors" "io" "net/http" "net/url" "sort" "strconv" "strings" ) // Multi represents an unfinished multipart upload. // // Multipart uploads allow sending big objects in smaller chunks. // After all parts have been sent, the upload must be explicitly // completed by calling Complete with the list of parts. // // See http://goo.gl/vJfTG for an overview of multipart uploads. type Multi struct { Bucket *Bucket Key string UploadId string } // That's the default. Here just for testing. var listMultiMax = 1000 type listMultiResp struct { NextKeyMarker string NextUploadIdMarker string IsTruncated bool Upload []Multi CommonPrefixes []string `xml:"CommonPrefixes>Prefix"` } // ListMulti returns the list of unfinished multipart uploads in b. // // The prefix parameter limits the response to keys that begin with the // specified prefix. You can use prefixes to separate a bucket into different // groupings of keys (to get the feeling of folders, for example). // // The delim parameter causes the response to group all of the keys that // share a common prefix up to the next delimiter in a single entry within // the CommonPrefixes field. You can use delimiters to separate a bucket // into different groupings of keys, similar to how folders would work. // // See http://goo.gl/ePioY for details. func (b *Bucket) ListMulti(prefix, delim string) (multis []*Multi, prefixes []string, err error) { params := map[string][]string{ "uploads": {""}, "max-uploads": {strconv.FormatInt(int64(listMultiMax), 10)}, "prefix": {prefix}, "delimiter": {delim}, } for attempt := attempts.Start(); attempt.Next(); { req := &request{ method: "GET", bucket: b.Name, params: params, } var resp listMultiResp err := b.S3.query(req, &resp) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return nil, nil, err } for i := range resp.Upload { multi := &resp.Upload[i] multi.Bucket = b multis = append(multis, multi) } prefixes = append(prefixes, resp.CommonPrefixes...) if !resp.IsTruncated { return multis, prefixes, nil } params["key-marker"] = []string{resp.NextKeyMarker} params["upload-id-marker"] = []string{resp.NextUploadIdMarker} attempt = attempts.Start() // Last request worked. } panic("unreachable") } // Multi returns a multipart upload handler for the provided key // inside b. If a multipart upload exists for key, it is returned, // otherwise a new multipart upload is initiated with contType and perm. func (b *Bucket) Multi(key, contType string, perm ACL, options Options) (*Multi, error) { multis, _, err := b.ListMulti(key, "") if err != nil && !hasCode(err, "NoSuchUpload") { return nil, err } for _, m := range multis { if m.Key == key { return m, nil } } return b.InitMulti(key, contType, perm, options) } // InitMulti initializes a new multipart upload at the provided // key inside b and returns a value for manipulating it. // // See http://goo.gl/XP8kL for details. func (b *Bucket) InitMulti(key string, contType string, perm ACL, options Options) (*Multi, error) { headers := map[string][]string{ "Content-Type": {contType}, "Content-Length": {"0"}, "x-amz-acl": {string(perm)}, } options.addHeaders(headers) params := map[string][]string{ "uploads": {""}, } req := &request{ method: "POST", bucket: b.Name, path: key, headers: headers, params: params, } var err error var resp struct { UploadId string `xml:"UploadId"` } for attempt := attempts.Start(); attempt.Next(); { err = b.S3.query(req, &resp) if !shouldRetry(err) { break } } if err != nil { return nil, err } return &Multi{Bucket: b, Key: key, UploadId: resp.UploadId}, nil } func (m *Multi) PutPartCopy(n int, options CopyOptions, source string) (*CopyObjectResult, Part, error) { headers := map[string][]string{ "x-amz-copy-source": {url.QueryEscape(source)}, } options.addHeaders(headers) params := map[string][]string{ "uploadId": {m.UploadId}, "partNumber": {strconv.FormatInt(int64(n), 10)}, } sourceBucket := m.Bucket.S3.Bucket(strings.TrimRight(strings.SplitAfterN(source, "/", 2)[0], "/")) sourceMeta, err := sourceBucket.Head(strings.SplitAfterN(source, "/", 2)[1], nil) if err != nil { return nil, Part{}, err } for attempt := attempts.Start(); attempt.Next(); { req := &request{ method: "PUT", bucket: m.Bucket.Name, path: m.Key, headers: headers, params: params, } resp := &CopyObjectResult{} err = m.Bucket.S3.query(req, resp) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return nil, Part{}, err } if resp.ETag == "" { return nil, Part{}, errors.New("part upload succeeded with no ETag") } return resp, Part{n, resp.ETag, sourceMeta.ContentLength}, nil } panic("unreachable") } // PutPart sends part n of the multipart upload, reading all the content from r. // Each part, except for the last one, must be at least 5MB in size. // // See http://goo.gl/pqZer for details. func (m *Multi) PutPart(n int, r io.ReadSeeker) (Part, error) { partSize, _, md5b64, err := seekerInfo(r) if err != nil { return Part{}, err } return m.putPart(n, r, partSize, md5b64) } func (m *Multi) putPart(n int, r io.ReadSeeker, partSize int64, md5b64 string) (Part, error) { headers := map[string][]string{ "Content-Length": {strconv.FormatInt(partSize, 10)}, "Content-MD5": {md5b64}, } params := map[string][]string{ "uploadId": {m.UploadId}, "partNumber": {strconv.FormatInt(int64(n), 10)}, } for attempt := attempts.Start(); attempt.Next(); { _, err := r.Seek(0, 0) if err != nil { return Part{}, err } req := &request{ method: "PUT", bucket: m.Bucket.Name, path: m.Key, headers: headers, params: params, payload: r, } err = m.Bucket.S3.prepare(req) if err != nil { return Part{}, err } resp, err := m.Bucket.S3.run(req, nil) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return Part{}, err } etag := resp.Header.Get("ETag") if etag == "" { return Part{}, errors.New("part upload succeeded with no ETag") } return Part{n, etag, partSize}, nil } panic("unreachable") } func seekerInfo(r io.ReadSeeker) (size int64, md5hex string, md5b64 string, err error) { _, err = r.Seek(0, 0) if err != nil { return 0, "", "", err } digest := md5.New() size, err = io.Copy(digest, r) if err != nil { return 0, "", "", err } sum := digest.Sum(nil) md5hex = hex.EncodeToString(sum) md5b64 = base64.StdEncoding.EncodeToString(sum) return size, md5hex, md5b64, nil } type Part struct { N int `xml:"PartNumber"` ETag string Size int64 } type partSlice []Part func (s partSlice) Len() int { return len(s) } func (s partSlice) Less(i, j int) bool { return s[i].N < s[j].N } func (s partSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } type listPartsResp struct { NextPartNumberMarker string IsTruncated bool Part []Part } // That's the default. Here just for testing. var listPartsMax = 1000 // Kept for backcompatability. See the documentation for ListPartsFull func (m *Multi) ListParts() ([]Part, error) { return m.ListPartsFull(0, listPartsMax) } // ListParts returns the list of previously uploaded parts in m, // ordered by part number (Only parts with higher part numbers than // partNumberMarker will be listed). Only up to maxParts parts will be // returned. // // See http://goo.gl/ePioY for details. func (m *Multi) ListPartsFull(partNumberMarker int, maxParts int) ([]Part, error) { if maxParts > listPartsMax { maxParts = listPartsMax } params := map[string][]string{ "uploadId": {m.UploadId}, "max-parts": {strconv.FormatInt(int64(maxParts), 10)}, "part-number-marker": {strconv.FormatInt(int64(partNumberMarker), 10)}, } var parts partSlice for attempt := attempts.Start(); attempt.Next(); { req := &request{ method: "GET", bucket: m.Bucket.Name, path: m.Key, params: params, } var resp listPartsResp err := m.Bucket.S3.query(req, &resp) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return nil, err } parts = append(parts, resp.Part...) if !resp.IsTruncated { sort.Sort(parts) return parts, nil } params["part-number-marker"] = []string{resp.NextPartNumberMarker} attempt = attempts.Start() // Last request worked. } panic("unreachable") } type ReaderAtSeeker interface { io.ReaderAt io.ReadSeeker } // PutAll sends all of r via a multipart upload with parts no larger // than partSize bytes, which must be set to at least 5MB. // Parts previously uploaded are either reused if their checksum // and size match the new part, or otherwise overwritten with the // new content. // PutAll returns all the parts of m (reused or not). func (m *Multi) PutAll(r ReaderAtSeeker, partSize int64) ([]Part, error) { old, err := m.ListParts() if err != nil && !hasCode(err, "NoSuchUpload") { return nil, err } reuse := 0 // Index of next old part to consider reusing. current := 1 // Part number of latest good part handled. totalSize, err := r.Seek(0, 2) if err != nil { return nil, err } first := true // Must send at least one empty part if the file is empty. var result []Part NextSection: for offset := int64(0); offset < totalSize || first; offset += partSize { first = false if offset+partSize > totalSize { partSize = totalSize - offset } section := io.NewSectionReader(r, offset, partSize) _, md5hex, md5b64, err := seekerInfo(section) if err != nil { return nil, err } for reuse < len(old) && old[reuse].N <= current { // Looks like this part was already sent. part := &old[reuse] etag := `"` + md5hex + `"` if part.N == current && part.Size == partSize && part.ETag == etag { // Checksum matches. Reuse the old part. result = append(result, *part) current++ continue NextSection } reuse++ } // Part wasn't found or doesn't match. Send it. part, err := m.putPart(current, section, partSize, md5b64) if err != nil { return nil, err } result = append(result, part) current++ } return result, nil } type completeUpload struct { XMLName xml.Name `xml:"CompleteMultipartUpload"` Parts completeParts `xml:"Part"` } type completePart struct { PartNumber int ETag string } type completeParts []completePart func (p completeParts) Len() int { return len(p) } func (p completeParts) Less(i, j int) bool { return p[i].PartNumber < p[j].PartNumber } func (p completeParts) Swap(i, j int) { p[i], p[j] = p[j], p[i] } // We can't know in advance whether we'll have an Error or a // CompleteMultipartUploadResult, so this structure is just a placeholder to // know the name of the XML object. type completeUploadResp struct { XMLName xml.Name InnerXML string `xml:",innerxml"` } // Complete assembles the given previously uploaded parts into the // final object. This operation may take several minutes. // // See http://goo.gl/2Z7Tw for details. func (m *Multi) Complete(parts []Part) error { params := map[string][]string{ "uploadId": {m.UploadId}, } c := completeUpload{} for _, p := range parts { c.Parts = append(c.Parts, completePart{p.N, p.ETag}) } sort.Sort(c.Parts) data, err := xml.Marshal(&c) if err != nil { return err } for attempt := attempts.Start(); attempt.Next(); { req := &request{ method: "POST", bucket: m.Bucket.Name, path: m.Key, params: params, payload: bytes.NewReader(data), } var resp completeUploadResp if m.Bucket.Region.Name == "generic" { headers := make(http.Header) headers.Add("Content-Length", strconv.FormatInt(int64(len(data)), 10)) req.headers = headers } err := m.Bucket.S3.query(req, &resp) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return err } // A 200 error code does not guarantee that there were no errors (see // http://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadComplete.html ), // so first figure out what kind of XML "object" we are dealing with. if resp.XMLName.Local == "Error" { // S3.query does the unmarshalling for us, so we can't unmarshal // again in a different struct... So we need to duct-tape back the // original XML back together. fullErrorXml := "" + resp.InnerXML + "" s3err := &Error{} if err := xml.Unmarshal([]byte(fullErrorXml), s3err); err != nil { return err } return s3err } if resp.XMLName.Local == "CompleteMultipartUploadResult" { // FIXME: One could probably add a CompleteFull method returning the // actual contents of the CompleteMultipartUploadResult object. return nil } return errors.New("Invalid XML struct returned: " + resp.XMLName.Local) } panic("unreachable") } // Abort deletes an unifinished multipart upload and any previously // uploaded parts for it. // // After a multipart upload is aborted, no additional parts can be // uploaded using it. However, if any part uploads are currently in // progress, those part uploads might or might not succeed. As a result, // it might be necessary to abort a given multipart upload multiple // times in order to completely free all storage consumed by all parts. // // NOTE: If the described scenario happens to you, please report back to // the goamz authors with details. In the future such retrying should be // handled internally, but it's not clear what happens precisely (Is an // error returned? Is the issue completely undetectable?). // // See http://goo.gl/dnyJw for details. func (m *Multi) Abort() error { params := map[string][]string{ "uploadId": {m.UploadId}, } for attempt := attempts.Start(); attempt.Next(); { req := &request{ method: "DELETE", bucket: m.Bucket.Name, path: m.Key, params: params, } err := m.Bucket.S3.query(req, nil) if shouldRetry(err) && attempt.HasNext() { continue } return err } panic("unreachable") } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/s3/s3.go000066400000000000000000001036131313450123100245620ustar00rootroot00000000000000// // goamz - Go packages to interact with the Amazon Web Services. // // https://wiki.ubuntu.com/goamz // // Copyright (c) 2011 Canonical Ltd. // // Written by Gustavo Niemeyer // package s3 import ( "bytes" "crypto/hmac" "crypto/md5" "crypto/sha1" "encoding/base64" "encoding/xml" "fmt" "io" "io/ioutil" "log" "net" "net/http" "net/http/httputil" "net/url" "path" "strconv" "strings" "time" "github.com/docker/goamz/aws" ) const debug = false // The S3 type encapsulates operations with an S3 region. type S3 struct { aws.Auth aws.Region Signature int Client *http.Client private byte // Reserve the right of using private data. } // The Bucket type encapsulates operations with an S3 bucket. type Bucket struct { *S3 Name string } // The Owner type represents the owner of the object in an S3 bucket. type Owner struct { ID string DisplayName string } // Fold options into an Options struct // type Options struct { SSE bool SSEKMS bool SSEKMSKeyId string SSECustomerAlgorithm string SSECustomerKey string SSECustomerKeyMD5 string Meta map[string][]string ContentEncoding string CacheControl string RedirectLocation string ContentMD5 string ContentDisposition string Range string StorageClass StorageClass // What else? } type CopyOptions struct { Options CopySourceOptions string MetadataDirective string ContentType string } // CopyObjectResult is the output from a Copy request type CopyObjectResult struct { ETag string LastModified string } var attempts = aws.AttemptStrategy{ Min: 5, Total: 5 * time.Second, Delay: 200 * time.Millisecond, } // New creates a new S3. func New(auth aws.Auth, region aws.Region) *S3 { return &S3{ Auth: auth, Region: region, Signature: aws.V2Signature, Client: http.DefaultClient, private: 0, } } // Bucket returns a Bucket with the given name. func (s3 *S3) Bucket(name string) *Bucket { if s3.Region.S3BucketEndpoint != "" || s3.Region.S3LowercaseBucket { name = strings.ToLower(name) } return &Bucket{s3, name} } type BucketInfo struct { Name string CreationDate string } type GetServiceResp struct { Owner Owner Buckets []BucketInfo `xml:">Bucket"` } // GetService gets a list of all buckets owned by an account. // // See http://goo.gl/wbHkGj for details. func (s3 *S3) GetService() (*GetServiceResp, error) { bucket := s3.Bucket("") r, err := bucket.Get("") if err != nil { return nil, err } // Parse the XML response. var resp GetServiceResp if err = xml.Unmarshal(r, &resp); err != nil { return nil, err } return &resp, nil } var createBucketConfiguration = ` %s ` // locationConstraint returns an io.Reader specifying a LocationConstraint if // required for the region. // // See http://goo.gl/bh9Kq for details. func (s3 *S3) locationConstraint() io.Reader { constraint := "" if s3.Region.S3LocationConstraint { constraint = fmt.Sprintf(createBucketConfiguration, s3.Region.Name) } return strings.NewReader(constraint) } type ACL string const ( Private = ACL("private") PublicRead = ACL("public-read") PublicReadWrite = ACL("public-read-write") AuthenticatedRead = ACL("authenticated-read") BucketOwnerRead = ACL("bucket-owner-read") BucketOwnerFull = ACL("bucket-owner-full-control") ) type StorageClass string const ( ReducedRedundancy = StorageClass("REDUCED_REDUNDANCY") StandardStorage = StorageClass("STANDARD") ) type ServerSideEncryption string const ( S3Managed = ServerSideEncryption("AES256") KMSManaged = ServerSideEncryption("aws:kms") ) // PutBucket creates a new bucket. // // See http://goo.gl/ndjnR for details. func (b *Bucket) PutBucket(perm ACL) error { headers := map[string][]string{ "x-amz-acl": {string(perm)}, } req := &request{ method: "PUT", bucket: b.Name, path: "/", headers: headers, payload: b.locationConstraint(), } return b.S3.query(req, nil) } // DelBucket removes an existing S3 bucket. All objects in the bucket must // be removed before the bucket itself can be removed. // // See http://goo.gl/GoBrY for details. func (b *Bucket) DelBucket() (err error) { req := &request{ method: "DELETE", bucket: b.Name, path: "/", } for attempt := attempts.Start(); attempt.Next(); { err = b.S3.query(req, nil) if !shouldRetry(err) { break } } return err } // Get retrieves an object from an S3 bucket. // // See http://goo.gl/isCO7 for details. func (b *Bucket) Get(path string) (data []byte, err error) { body, err := b.GetReader(path) if err != nil { return nil, err } data, err = ioutil.ReadAll(body) body.Close() return data, err } // GetReader retrieves an object from an S3 bucket, // returning the body of the HTTP response. // It is the caller's responsibility to call Close on rc when // finished reading. func (b *Bucket) GetReader(path string) (rc io.ReadCloser, err error) { resp, err := b.GetResponse(path) if resp != nil { return resp.Body, err } return nil, err } // GetResponse retrieves an object from an S3 bucket, // returning the HTTP response. // It is the caller's responsibility to call Close on rc when // finished reading func (b *Bucket) GetResponse(path string) (resp *http.Response, err error) { return b.GetResponseWithHeaders(path, make(http.Header)) } // GetReaderWithHeaders retrieves an object from an S3 bucket // Accepts custom headers to be sent as the second parameter // returning the body of the HTTP response. // It is the caller's responsibility to call Close on rc when // finished reading func (b *Bucket) GetResponseWithHeaders(path string, headers map[string][]string) (resp *http.Response, err error) { req := &request{ bucket: b.Name, path: path, headers: headers, } err = b.S3.prepare(req) if err != nil { return nil, err } for attempt := attempts.Start(); attempt.Next(); { resp, err := b.S3.run(req, nil) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return nil, err } return resp, nil } panic("unreachable") } // Exists checks whether or not an object exists on an S3 bucket using a HEAD request. func (b *Bucket) Exists(path string) (exists bool, err error) { req := &request{ method: "HEAD", bucket: b.Name, path: path, } err = b.S3.prepare(req) if err != nil { return } for attempt := attempts.Start(); attempt.Next(); { resp, err := b.S3.run(req, nil) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { // We can treat a 403 or 404 as non existance if e, ok := err.(*Error); ok && (e.StatusCode == 403 || e.StatusCode == 404) { return false, nil } return false, err } if resp.StatusCode/100 == 2 { exists = true } if resp.Body != nil { resp.Body.Close() } return exists, err } return false, fmt.Errorf("S3 Currently Unreachable") } // Head HEADs an object in the S3 bucket, returns the response with // no body see http://bit.ly/17K1ylI func (b *Bucket) Head(path string, headers map[string][]string) (*http.Response, error) { req := &request{ method: "HEAD", bucket: b.Name, path: path, headers: headers, } err := b.S3.prepare(req) if err != nil { return nil, err } for attempt := attempts.Start(); attempt.Next(); { resp, err := b.S3.run(req, nil) if shouldRetry(err) && attempt.HasNext() { continue } if err != nil { return nil, err } return resp, err } return nil, fmt.Errorf("S3 Currently Unreachable") } // Put inserts an object into the S3 bucket. // // See http://goo.gl/FEBPD for details. func (b *Bucket) Put(path string, data []byte, contType string, perm ACL, options Options) error { body := bytes.NewBuffer(data) return b.PutReader(path, body, int64(len(data)), contType, perm, options) } // PutCopy puts a copy of an object given by the key path into bucket b using b.Path as the target key func (b *Bucket) PutCopy(path string, perm ACL, options CopyOptions, source string) (*CopyObjectResult, error) { headers := map[string][]string{ "x-amz-acl": {string(perm)}, "x-amz-copy-source": {escapePath(source)}, } options.addHeaders(headers) req := &request{ method: "PUT", bucket: b.Name, path: path, headers: headers, } resp := &CopyObjectResult{} err := b.S3.query(req, resp) if err != nil { return resp, err } return resp, nil } // PutReader inserts an object into the S3 bucket by consuming data // from r until EOF. func (b *Bucket) PutReader(path string, r io.Reader, length int64, contType string, perm ACL, options Options) error { headers := map[string][]string{ "Content-Length": {strconv.FormatInt(length, 10)}, "Content-Type": {contType}, "x-amz-acl": {string(perm)}, } options.addHeaders(headers) req := &request{ method: "PUT", bucket: b.Name, path: path, headers: headers, payload: r, } return b.S3.query(req, nil) } // addHeaders adds o's specified fields to headers func (o Options) addHeaders(headers map[string][]string) { if o.SSE { headers["x-amz-server-side-encryption"] = []string{string(S3Managed)} } else if o.SSEKMS { headers["x-amz-server-side-encryption"] = []string{string(KMSManaged)} if len(o.SSEKMSKeyId) != 0 { headers["x-amz-server-side-encryption-aws-kms-key-id"] = []string{o.SSEKMSKeyId} } } else if len(o.SSECustomerAlgorithm) != 0 && len(o.SSECustomerKey) != 0 && len(o.SSECustomerKeyMD5) != 0 { // Amazon-managed keys and customer-managed keys are mutually exclusive headers["x-amz-server-side-encryption-customer-algorithm"] = []string{o.SSECustomerAlgorithm} headers["x-amz-server-side-encryption-customer-key"] = []string{o.SSECustomerKey} headers["x-amz-server-side-encryption-customer-key-MD5"] = []string{o.SSECustomerKeyMD5} } if len(o.Range) != 0 { headers["Range"] = []string{o.Range} } if len(o.ContentEncoding) != 0 { headers["Content-Encoding"] = []string{o.ContentEncoding} } if len(o.CacheControl) != 0 { headers["Cache-Control"] = []string{o.CacheControl} } if len(o.ContentMD5) != 0 { headers["Content-MD5"] = []string{o.ContentMD5} } if len(o.RedirectLocation) != 0 { headers["x-amz-website-redirect-location"] = []string{o.RedirectLocation} } if len(o.ContentDisposition) != 0 { headers["Content-Disposition"] = []string{o.ContentDisposition} } if len(o.StorageClass) != 0 { headers["x-amz-storage-class"] = []string{string(o.StorageClass)} } for k, v := range o.Meta { headers["x-amz-meta-"+k] = v } } // addHeaders adds o's specified fields to headers func (o CopyOptions) addHeaders(headers map[string][]string) { o.Options.addHeaders(headers) if len(o.MetadataDirective) != 0 { headers["x-amz-metadata-directive"] = []string{o.MetadataDirective} } if len(o.CopySourceOptions) != 0 { headers["x-amz-copy-source-range"] = []string{o.CopySourceOptions} } if len(o.ContentType) != 0 { headers["Content-Type"] = []string{o.ContentType} } } func makeXmlBuffer(doc []byte) *bytes.Buffer { buf := new(bytes.Buffer) buf.WriteString(xml.Header) buf.Write(doc) return buf } type IndexDocument struct { Suffix string `xml:"Suffix"` } type ErrorDocument struct { Key string `xml:"Key"` } type RoutingRule struct { ConditionKeyPrefixEquals string `xml:"Condition>KeyPrefixEquals"` RedirectReplaceKeyPrefixWith string `xml:"Redirect>ReplaceKeyPrefixWith,omitempty"` RedirectReplaceKeyWith string `xml:"Redirect>ReplaceKeyWith,omitempty"` } type RedirectAllRequestsTo struct { HostName string `xml:"HostName"` Protocol string `xml:"Protocol,omitempty"` } type WebsiteConfiguration struct { XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ WebsiteConfiguration"` IndexDocument *IndexDocument `xml:"IndexDocument,omitempty"` ErrorDocument *ErrorDocument `xml:"ErrorDocument,omitempty"` RoutingRules *[]RoutingRule `xml:"RoutingRules>RoutingRule,omitempty"` RedirectAllRequestsTo *RedirectAllRequestsTo `xml:"RedirectAllRequestsTo,omitempty"` } // PutBucketWebsite configures a bucket as a website. // // See http://goo.gl/TpRlUy for details. func (b *Bucket) PutBucketWebsite(configuration WebsiteConfiguration) error { doc, err := xml.Marshal(configuration) if err != nil { return err } buf := makeXmlBuffer(doc) return b.PutBucketSubresource("website", buf, int64(buf.Len())) } func (b *Bucket) PutBucketSubresource(subresource string, r io.Reader, length int64) error { headers := map[string][]string{ "Content-Length": {strconv.FormatInt(length, 10)}, } req := &request{ path: "/", method: "PUT", bucket: b.Name, headers: headers, payload: r, params: url.Values{subresource: {""}}, } return b.S3.query(req, nil) } // Del removes an object from the S3 bucket. // // See http://goo.gl/APeTt for details. func (b *Bucket) Del(path string) error { req := &request{ method: "DELETE", bucket: b.Name, path: path, } return b.S3.query(req, nil) } type Delete struct { Quiet bool `xml:"Quiet,omitempty"` Objects []Object `xml:"Object"` } type Object struct { Key string `xml:"Key"` VersionId string `xml:"VersionId,omitempty"` } // DelMulti removes up to 1000 objects from the S3 bucket. // // See http://goo.gl/jx6cWK for details. func (b *Bucket) DelMulti(objects Delete) error { doc, err := xml.Marshal(objects) if err != nil { return err } buf := makeXmlBuffer(doc) digest := md5.New() size, err := digest.Write(buf.Bytes()) if err != nil { return err } headers := map[string][]string{ "Content-Length": {strconv.FormatInt(int64(size), 10)}, "Content-MD5": {base64.StdEncoding.EncodeToString(digest.Sum(nil))}, "Content-Type": {"text/xml"}, } req := &request{ path: "/", method: "POST", params: url.Values{"delete": {""}}, bucket: b.Name, headers: headers, payload: buf, } return b.S3.query(req, nil) } // The ListResp type holds the results of a List bucket operation. type ListResp struct { Name string Prefix string Delimiter string Marker string MaxKeys int // IsTruncated is true if the results have been truncated because // there are more keys and prefixes than can fit in MaxKeys. // N.B. this is the opposite sense to that documented (incorrectly) in // http://goo.gl/YjQTc IsTruncated bool Contents []Key CommonPrefixes []string `xml:">Prefix"` // if IsTruncated is true, pass NextMarker as marker argument to List() // to get the next set of keys NextMarker string } // The Key type represents an item stored in an S3 bucket. type Key struct { Key string LastModified string Size int64 // ETag gives the hex-encoded MD5 sum of the contents, // surrounded with double-quotes. ETag string StorageClass string Owner Owner } // List returns information about objects in an S3 bucket. // // The prefix parameter limits the response to keys that begin with the // specified prefix. // // The delim parameter causes the response to group all of the keys that // share a common prefix up to the next delimiter in a single entry within // the CommonPrefixes field. You can use delimiters to separate a bucket // into different groupings of keys, similar to how folders would work. // // The marker parameter specifies the key to start with when listing objects // in a bucket. Amazon S3 lists objects in alphabetical order and // will return keys alphabetically greater than the marker. // // The max parameter specifies how many keys + common prefixes to return in // the response. The default is 1000. // // For example, given these keys in a bucket: // // index.html // index2.html // photos/2006/January/sample.jpg // photos/2006/February/sample2.jpg // photos/2006/February/sample3.jpg // photos/2006/February/sample4.jpg // // Listing this bucket with delimiter set to "/" would yield the // following result: // // &ListResp{ // Name: "sample-bucket", // MaxKeys: 1000, // Delimiter: "/", // Contents: []Key{ // {Key: "index.html", "index2.html"}, // }, // CommonPrefixes: []string{ // "photos/", // }, // } // // Listing the same bucket with delimiter set to "/" and prefix set to // "photos/2006/" would yield the following result: // // &ListResp{ // Name: "sample-bucket", // MaxKeys: 1000, // Delimiter: "/", // Prefix: "photos/2006/", // CommonPrefixes: []string{ // "photos/2006/February/", // "photos/2006/January/", // }, // } // // See http://goo.gl/YjQTc for details. func (b *Bucket) List(prefix, delim, marker string, max int) (result *ListResp, err error) { params := map[string][]string{ "prefix": {prefix}, "delimiter": {delim}, "marker": {marker}, } if max != 0 { params["max-keys"] = []string{strconv.FormatInt(int64(max), 10)} } req := &request{ bucket: b.Name, params: params, } result = &ListResp{} for attempt := attempts.Start(); attempt.Next(); { err = b.S3.query(req, result) if !shouldRetry(err) { break } } if err != nil { return nil, err } // if NextMarker is not returned, it should be set to the name of last key, // so let's do it so that each caller doesn't have to if result.IsTruncated && result.NextMarker == "" { n := len(result.Contents) if n > 0 { result.NextMarker = result.Contents[n-1].Key } } return result, nil } // The VersionsResp type holds the results of a list bucket Versions operation. type VersionsResp struct { Name string Prefix string KeyMarker string VersionIdMarker string MaxKeys int Delimiter string IsTruncated bool Versions []Version `xml:"Version"` CommonPrefixes []string `xml:">Prefix"` } // The Version type represents an object version stored in an S3 bucket. type Version struct { Key string VersionId string IsLatest bool LastModified string // ETag gives the hex-encoded MD5 sum of the contents, // surrounded with double-quotes. ETag string Size int64 Owner Owner StorageClass string } func (b *Bucket) Versions(prefix, delim, keyMarker string, versionIdMarker string, max int) (result *VersionsResp, err error) { params := map[string][]string{ "versions": {""}, "prefix": {prefix}, "delimiter": {delim}, } if len(versionIdMarker) != 0 { params["version-id-marker"] = []string{versionIdMarker} } if len(keyMarker) != 0 { params["key-marker"] = []string{keyMarker} } if max != 0 { params["max-keys"] = []string{strconv.FormatInt(int64(max), 10)} } req := &request{ bucket: b.Name, params: params, } result = &VersionsResp{} for attempt := attempts.Start(); attempt.Next(); { err = b.S3.query(req, result) if !shouldRetry(err) { break } } if err != nil { return nil, err } return result, nil } type GetLocationResp struct { Location string `xml:",innerxml"` } func (b *Bucket) Location() (string, error) { r, err := b.Get("/?location") if err != nil { return "", err } // Parse the XML response. var resp GetLocationResp if err = xml.Unmarshal(r, &resp); err != nil { return "", err } if resp.Location == "" { return "us-east-1", nil } else { return resp.Location, nil } } // URL returns a non-signed URL that allows retriving the // object at path. It only works if the object is publicly // readable (see SignedURL). func (b *Bucket) URL(path string) string { req := &request{ bucket: b.Name, path: path, } err := b.S3.prepare(req) if err != nil { panic(err) } u, err := req.url() if err != nil { panic(err) } u.RawQuery = "" return u.String() } // SignedURL returns a signed URL that allows anyone holding the URL // to retrieve the object at path. The signature is valid until expires. func (b *Bucket) SignedURL(path string, expires time.Time) string { return b.SignedURLWithArgs(path, expires, nil, nil) } // SignedURLWithArgs returns a signed URL that allows anyone holding the URL // to retrieve the object at path. The signature is valid until expires. func (b *Bucket) SignedURLWithArgs(path string, expires time.Time, params url.Values, headers http.Header) string { return b.SignedURLWithMethod("GET", path, expires, params, headers) } // SignedURLWithMethod returns a signed URL that allows anyone holding the URL // to either retrieve the object at path or make a HEAD request against it. The signature is valid until expires. func (b *Bucket) SignedURLWithMethod(method, path string, expires time.Time, params url.Values, headers http.Header) string { var uv = url.Values{} if params != nil { uv = params } if b.S3.Signature == aws.V2Signature { uv.Set("Expires", strconv.FormatInt(expires.Unix(), 10)) } else { uv.Set("X-Amz-Expires", strconv.FormatInt(expires.Unix()-time.Now().Unix(), 10)) } req := &request{ method: method, bucket: b.Name, path: path, params: uv, headers: headers, } err := b.S3.prepare(req) if err != nil { panic(err) } u, err := req.url() if err != nil { panic(err) } if b.S3.Auth.Token() != "" && b.S3.Signature == aws.V2Signature { return u.String() + "&x-amz-security-token=" + url.QueryEscape(req.headers["X-Amz-Security-Token"][0]) } else { return u.String() } } // UploadSignedURL returns a signed URL that allows anyone holding the URL // to upload the object at path. The signature is valid until expires. // contenttype is a string like image/png // name is the resource name in s3 terminology like images/ali.png [obviously excluding the bucket name itself] func (b *Bucket) UploadSignedURL(name, method, content_type string, expires time.Time) string { expire_date := expires.Unix() if method != "POST" { method = "PUT" } a := b.S3.Auth tokenData := "" if a.Token() != "" { tokenData = "x-amz-security-token:" + a.Token() + "\n" } stringToSign := method + "\n\n" + content_type + "\n" + strconv.FormatInt(expire_date, 10) + "\n" + tokenData + "/" + path.Join(b.Name, name) secretKey := a.SecretKey accessId := a.AccessKey mac := hmac.New(sha1.New, []byte(secretKey)) mac.Write([]byte(stringToSign)) macsum := mac.Sum(nil) signature := base64.StdEncoding.EncodeToString([]byte(macsum)) signature = strings.TrimSpace(signature) var signedurl *url.URL var err error if b.Region.S3Endpoint != "" { signedurl, err = url.Parse(b.Region.S3Endpoint) name = b.Name + "/" + name } else { signedurl, err = url.Parse("https://" + b.Name + ".s3.amazonaws.com/") } if err != nil { log.Println("ERROR sining url for S3 upload", err) return "" } signedurl.Path = name params := url.Values{} params.Add("AWSAccessKeyId", accessId) params.Add("Expires", strconv.FormatInt(expire_date, 10)) params.Add("Signature", signature) if a.Token() != "" { params.Add("x-amz-security-token", a.Token()) } signedurl.RawQuery = params.Encode() return signedurl.String() } // PostFormArgs returns the action and input fields needed to allow anonymous // uploads to a bucket within the expiration limit // Additional conditions can be specified with conds func (b *Bucket) PostFormArgsEx(path string, expires time.Time, redirect string, conds []string) (action string, fields map[string]string) { conditions := make([]string, 0) fields = map[string]string{ "AWSAccessKeyId": b.Auth.AccessKey, "key": path, } if token := b.S3.Auth.Token(); token != "" { fields["x-amz-security-token"] = token conditions = append(conditions, fmt.Sprintf("{\"x-amz-security-token\": \"%s\"}", token)) } if conds != nil { conditions = append(conditions, conds...) } conditions = append(conditions, fmt.Sprintf("{\"key\": \"%s\"}", path)) conditions = append(conditions, fmt.Sprintf("{\"bucket\": \"%s\"}", b.Name)) if redirect != "" { conditions = append(conditions, fmt.Sprintf("{\"success_action_redirect\": \"%s\"}", redirect)) fields["success_action_redirect"] = redirect } vExpiration := expires.Format("2006-01-02T15:04:05Z") vConditions := strings.Join(conditions, ",") policy := fmt.Sprintf("{\"expiration\": \"%s\", \"conditions\": [%s]}", vExpiration, vConditions) policy64 := base64.StdEncoding.EncodeToString([]byte(policy)) fields["policy"] = policy64 signer := hmac.New(sha1.New, []byte(b.Auth.SecretKey)) signer.Write([]byte(policy64)) fields["signature"] = base64.StdEncoding.EncodeToString(signer.Sum(nil)) action = fmt.Sprintf("%s/%s/", b.S3.Region.S3Endpoint, b.Name) return } // PostFormArgs returns the action and input fields needed to allow anonymous // uploads to a bucket within the expiration limit func (b *Bucket) PostFormArgs(path string, expires time.Time, redirect string) (action string, fields map[string]string) { return b.PostFormArgsEx(path, expires, redirect, nil) } type request struct { method string bucket string path string params url.Values headers http.Header baseurl string payload io.Reader prepared bool } func (req *request) url() (*url.URL, error) { u, err := url.Parse(req.baseurl) if err != nil { return nil, fmt.Errorf("bad S3 endpoint URL %q: %v", req.baseurl, err) } u.RawQuery = req.params.Encode() u.Path = req.path return u, nil } // query prepares and runs the req request. // If resp is not nil, the XML data contained in the response // body will be unmarshalled on it. func (s3 *S3) query(req *request, resp interface{}) error { err := s3.prepare(req) if err != nil { return err } r, err := s3.run(req, resp) if r != nil && r.Body != nil { r.Body.Close() } return err } // queryV4Signprepares and runs the req request, signed with aws v4 signatures. // If resp is not nil, the XML data contained in the response // body will be unmarshalled on it. func (s3 *S3) queryV4Sign(req *request, resp interface{}) error { if req.headers == nil { req.headers = map[string][]string{} } err := s3.setBaseURL(req) if err != nil { return err } hreq, err := s3.setupHttpRequest(req) if err != nil { return err } // req.Host must be set for V4 signature calculation hreq.Host = hreq.URL.Host signer := aws.NewV4Signer(s3.Auth, "s3", s3.Region) signer.IncludeXAmzContentSha256 = true signer.Sign(hreq) _, err = s3.doHttpRequest(hreq, resp) return err } // Sets baseurl on req from bucket name and the region endpoint func (s3 *S3) setBaseURL(req *request) error { if req.bucket == "" { req.baseurl = s3.Region.S3Endpoint } else { req.baseurl = s3.Region.S3BucketEndpoint if req.baseurl == "" { // Use the path method to address the bucket. req.baseurl = s3.Region.S3Endpoint req.path = "/" + req.bucket + req.path } else { // Just in case, prevent injection. if strings.IndexAny(req.bucket, "/:@") >= 0 { return fmt.Errorf("bad S3 bucket: %q", req.bucket) } req.baseurl = strings.Replace(req.baseurl, "${bucket}", req.bucket, -1) } } return nil } // partiallyEscapedPath partially escapes the S3 path allowing for all S3 REST API calls. // // Some commands including: // GET Bucket acl http://goo.gl/aoXflF // GET Bucket cors http://goo.gl/UlmBdx // GET Bucket lifecycle http://goo.gl/8Fme7M // GET Bucket policy http://goo.gl/ClXIo3 // GET Bucket location http://goo.gl/5lh8RD // GET Bucket Logging http://goo.gl/sZ5ckF // GET Bucket notification http://goo.gl/qSSZKD // GET Bucket tagging http://goo.gl/QRvxnM // require the first character after the bucket name in the path to be a literal '?' and // not the escaped hex representation '%3F'. func partiallyEscapedPath(path string) string { pathEscapedAndSplit := strings.Split((&url.URL{Path: path}).String(), "/") if len(pathEscapedAndSplit) >= 3 { if len(pathEscapedAndSplit[2]) >= 3 { // Check for the one "?" that should not be escaped. if pathEscapedAndSplit[2][0:3] == "%3F" { pathEscapedAndSplit[2] = "?" + pathEscapedAndSplit[2][3:] } } } return strings.Replace(strings.Join(pathEscapedAndSplit, "/"), "+", "%2B", -1) } // prepare sets up req to be delivered to S3. func (s3 *S3) prepare(req *request) error { // Copy so they can be mutated without affecting on retries. params := make(url.Values) headers := make(http.Header) for k, v := range req.params { params[k] = v } for k, v := range req.headers { headers[k] = v } req.params = params req.headers = headers if !req.prepared { req.prepared = true if req.method == "" { req.method = "GET" } if !strings.HasPrefix(req.path, "/") { req.path = "/" + req.path } err := s3.setBaseURL(req) if err != nil { return err } } if s3.Signature == aws.V2Signature && s3.Auth.Token() != "" { req.headers["X-Amz-Security-Token"] = []string{s3.Auth.Token()} } else if s3.Auth.Token() != "" { req.params.Set("X-Amz-Security-Token", s3.Auth.Token()) } if s3.Signature == aws.V2Signature { // Always sign again as it's not clear how far the // server has handled a previous attempt. u, err := url.Parse(req.baseurl) if err != nil { return err } signpathPartiallyEscaped := partiallyEscapedPath(req.path) if strings.IndexAny(s3.Region.S3BucketEndpoint, "${bucket}") >= 0 { signpathPartiallyEscaped = "/" + req.bucket + signpathPartiallyEscaped } req.headers["Host"] = []string{u.Host} req.headers["Date"] = []string{time.Now().In(time.UTC).Format(time.RFC1123)} sign(s3.Auth, req.method, signpathPartiallyEscaped, req.params, req.headers) } else { hreq, err := s3.setupHttpRequest(req) if err != nil { return err } hreq.Host = hreq.URL.Host signer := aws.NewV4Signer(s3.Auth, "s3", s3.Region) signer.IncludeXAmzContentSha256 = true signer.Sign(hreq) req.payload = hreq.Body if _, ok := headers["Content-Length"]; ok { req.headers["Content-Length"] = headers["Content-Length"] } } return nil } // Prepares an *http.Request for doHttpRequest func (s3 *S3) setupHttpRequest(req *request) (*http.Request, error) { // Copy so that signing the http request will not mutate it headers := make(http.Header) for k, v := range req.headers { headers[k] = v } req.headers = headers u, err := req.url() if err != nil { return nil, err } if s3.Region.Name != "generic" { u.Opaque = fmt.Sprintf("//%s%s", u.Host, partiallyEscapedPath(u.Path)) } hreq := http.Request{ URL: u, Method: req.method, ProtoMajor: 1, ProtoMinor: 1, Header: req.headers, Form: req.params, } if v, ok := req.headers["Content-Length"]; ok { hreq.ContentLength, _ = strconv.ParseInt(v[0], 10, 64) delete(req.headers, "Content-Length") } if req.payload != nil { hreq.Body = ioutil.NopCloser(req.payload) } return &hreq, nil } // doHttpRequest sends hreq and returns the http response from the server. // If resp is not nil, the XML data contained in the response // body will be unmarshalled on it. func (s3 *S3) doHttpRequest(hreq *http.Request, resp interface{}) (*http.Response, error) { hresp, err := s3.Client.Do(hreq) if err != nil { return nil, err } if debug { dump, _ := httputil.DumpResponse(hresp, true) log.Printf("} -> %s\n", dump) } if hresp.StatusCode != 200 && hresp.StatusCode != 204 && hresp.StatusCode != 206 { return nil, buildError(hresp) } if resp != nil { err = xml.NewDecoder(hresp.Body).Decode(resp) hresp.Body.Close() if debug { log.Printf("goamz.s3> decoded xml into %#v", resp) } } return hresp, err } // run sends req and returns the http response from the server. // If resp is not nil, the XML data contained in the response // body will be unmarshalled on it. func (s3 *S3) run(req *request, resp interface{}) (*http.Response, error) { if debug { log.Printf("Running S3 request: %#v", req) } hreq, err := s3.setupHttpRequest(req) if err != nil { return nil, err } return s3.doHttpRequest(hreq, resp) } // Error represents an error in an operation with S3. type Error struct { StatusCode int // HTTP status code (200, 403, ...) Code string // EC2 error code ("UnsupportedOperation", ...) Message string // The human-oriented error message BucketName string RequestId string HostId string } func (e *Error) Error() string { return e.Message } func buildError(r *http.Response) error { if debug { log.Printf("got error (status code %v)", r.StatusCode) data, err := ioutil.ReadAll(r.Body) if err != nil { log.Printf("\tread error: %v", err) } else { log.Printf("\tdata:\n%s\n\n", data) } r.Body = ioutil.NopCloser(bytes.NewBuffer(data)) } err := Error{} // TODO return error if Unmarshal fails? xml.NewDecoder(r.Body).Decode(&err) r.Body.Close() err.StatusCode = r.StatusCode if err.Message == "" { err.Message = r.Status } if debug { log.Printf("err: %#v\n", err) } return &err } func shouldRetry(err error) bool { if err == nil { return false } switch err { case io.ErrUnexpectedEOF, io.EOF: return true } switch e := err.(type) { case *net.DNSError: return true case *net.OpError: switch e.Op { case "dial", "read", "write": return true } case *url.Error: // url.Error can be returned either by net/url if a URL cannot be // parsed, or by net/http if the response is closed before the headers // are received or parsed correctly. In that later case, e.Op is set to // the HTTP method name with the first letter uppercased. We don't want // to retry on POST operations, since those are not idempotent, all the // other ones should be safe to retry. The only case where all // operations are safe to retry are "dial" errors, since in that case // the POST request didn't make it to the server. if netErr, ok := e.Err.(*net.OpError); ok && netErr.Op == "dial" { return true } switch e.Op { case "Get", "Put", "Delete", "Head": return shouldRetry(e.Err) default: return false } case *Error: switch e.Code { case "InternalError", "NoSuchUpload", "NoSuchBucket": return true } switch e.StatusCode { case 500, 503, 504: return true } } return false } func hasCode(err error, code string) bool { s3err, ok := err.(*Error) return ok && s3err.Code == code } func escapePath(s string) string { return (&url.URL{Path: s}).String() } docker-registry-2.6.2~ds1/vendor/github.com/docker/goamz/s3/sign.go000066400000000000000000000060271313450123100251760ustar00rootroot00000000000000package s3 import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "github.com/docker/goamz/aws" "log" "sort" "strings" ) var b64 = base64.StdEncoding // ---------------------------------------------------------------------------- // S3 signing (http://goo.gl/G1LrK) var s3ParamsToSign = map[string]bool{ "acl": true, "location": true, "logging": true, "notification": true, "partNumber": true, "policy": true, "requestPayment": true, "torrent": true, "uploadId": true, "uploads": true, "versionId": true, "versioning": true, "versions": true, "response-content-type": true, "response-content-language": true, "response-expires": true, "response-cache-control": true, "response-content-disposition": true, "response-content-encoding": true, "website": true, "delete": true, } func sign(auth aws.Auth, method, canonicalPath string, params, headers map[string][]string) { var md5, ctype, date, xamz string var xamzDate bool var keys, sarray []string xheaders := make(map[string]string) for k, v := range headers { k = strings.ToLower(k) switch k { case "content-md5": md5 = v[0] case "content-type": ctype = v[0] case "date": if !xamzDate { date = v[0] } default: if strings.HasPrefix(k, "x-amz-") { keys = append(keys, k) xheaders[k] = strings.Join(v, ",") if k == "x-amz-date" { xamzDate = true date = "" } } } } if len(keys) > 0 { sort.StringSlice(keys).Sort() for i := range keys { key := keys[i] value := xheaders[key] sarray = append(sarray, key+":"+value) } xamz = strings.Join(sarray, "\n") + "\n" } expires := false if v, ok := params["Expires"]; ok { // Query string request authentication alternative. expires = true date = v[0] params["AWSAccessKeyId"] = []string{auth.AccessKey} } sarray = sarray[0:0] for k, v := range params { if s3ParamsToSign[k] { for _, vi := range v { if vi == "" { sarray = append(sarray, k) } else { // "When signing you do not encode these values." sarray = append(sarray, k+"="+vi) } } } } if len(sarray) > 0 { sort.StringSlice(sarray).Sort() canonicalPath = canonicalPath + "?" + strings.Join(sarray, "&") } payload := method + "\n" + md5 + "\n" + ctype + "\n" + date + "\n" + xamz + canonicalPath hash := hmac.New(sha1.New, []byte(auth.SecretKey)) hash.Write([]byte(payload)) signature := make([]byte, b64.EncodedLen(hash.Size())) b64.Encode(signature, hash.Sum(nil)) if expires { params["Signature"] = []string{string(signature)} } else { headers["Authorization"] = []string{"AWS " + auth.AccessKey + ":" + string(signature)} } if debug { log.Printf("Signature payload: %q", payload) log.Printf("Signature: %q", signature) } } docker-registry-2.6.2~ds1/vendor/rsc.io/000077500000000000000000000000001313450123100201275ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/000077500000000000000000000000001313450123100225035ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/LICENSE000066400000000000000000000027071313450123100235160ustar00rootroot00000000000000Copyright (c) 2009 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/README000066400000000000000000000142561313450123100233730ustar00rootroot00000000000000package letsencrypt // import "rsc.io/letsencrypt" Package letsencrypt obtains TLS certificates from LetsEncrypt.org. LetsEncrypt.org is a service that issues free SSL/TLS certificates to servers that can prove control over the given domain's DNS records or the servers pointed at by those records. Quick Start A complete HTTP/HTTPS web server using TLS certificates from LetsEncrypt.org, redirecting all HTTP access to HTTPS, and maintaining TLS certificates in a file letsencrypt.cache across server restarts. package main import ( "fmt" "log" "net/http" "rsc.io/letsencrypt" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, TLS!\n") }) var m letsencrypt.Manager if err := m.CacheFile("letsencrypt.cache"); err != nil { log.Fatal(err) } log.Fatal(m.Serve()) } Overview The fundamental type in this package is the Manager, which manages obtaining and refreshing a collection of TLS certificates, typically for use by an HTTPS server. The example above shows the most basic use of a Manager. The use can be customized by calling additional methods of the Manager. Registration A Manager m registers anonymously with LetsEncrypt.org, including agreeing to the letsencrypt.org terms of service, the first time it needs to obtain a certificate. To register with a particular email address and with the option of a prompt for agreement with the terms of service, call m.Register. GetCertificate The Manager's GetCertificate method returns certificates from the Manager's cache, filling the cache by requesting certificates from LetsEncrypt.org. In this way, a server with a tls.Config.GetCertificate set to m.GetCertificate will demand load a certificate for any host name it serves. To force loading of certificates ahead of time, install m.GetCertificate as before but then call m.Cert for each host name. A Manager can only obtain a certificate for a given host name if it can prove control of that host name to LetsEncrypt.org. By default it proves control by answering an HTTPS-based challenge: when the LetsEncrypt.org servers connect to the named host on port 443 (HTTPS), the TLS SNI handshake must use m.GetCertificate to obtain a per-host certificate. The most common way to satisfy this requirement is for the host name to resolve to the IP address of a (single) computer running m.ServeHTTPS, or at least running a Go TLS server with tls.Config.GetCertificate set to m.GetCertificate. However, other configurations are possible. For example, a group of machines could use an implementation of tls.Config.GetCertificate that cached certificates but handled cache misses by making RPCs to a Manager m on an elected leader machine. In typical usage, then, the setting of tls.Config.GetCertificate to m.GetCertificate serves two purposes: it provides certificates to the TLS server for ordinary serving, and it also answers challenges to prove ownership of the domains in order to obtain those certificates. To force the loading of a certificate for a given host into the Manager's cache, use m.Cert. Persistent Storage If a server always starts with a zero Manager m, the server effectively fetches a new certificate for each of its host name from LetsEncrypt.org on each restart. This is unfortunate both because the server cannot start if LetsEncrypt.org is unavailable and because LetsEncrypt.org limits how often it will issue a certificate for a given host name (at time of writing, the limit is 5 per week for a given host name). To save server state proactively to a cache file and to reload the server state from that same file when creating a new manager, call m.CacheFile with the name of the file to use. For alternate storage uses, m.Marshal returns the current state of the Manager as an opaque string, m.Unmarshal sets the state of the Manager using a string previously returned by m.Marshal (usually a different m), and m.Watch returns a channel that receives notifications about state changes. Limits To avoid hitting basic rate limits on LetsEncrypt.org, a given Manager limits all its interactions to at most one request every minute, with an initial allowed burst of 20 requests. By default, if GetCertificate is asked for a certificate it does not have, it will in turn ask LetsEncrypt.org for that certificate. This opens a potential attack where attackers connect to a server by IP address and pretend to be asking for an incorrect host name. Then GetCertificate will attempt to obtain a certificate for that host, incorrectly, eventually hitting LetsEncrypt.org's rate limit for certificate requests and making it impossible to obtain actual certificates. Because servers hold certificates for months at a time, however, an attack would need to be sustained over a time period of at least a month in order to cause real problems. To mitigate this kind of attack, a given Manager limits itself to an average of one certificate request for a new host every three hours, with an initial allowed burst of up to 20 requests. Long-running servers will therefore stay within the LetsEncrypt.org limit of 300 failed requests per month. Certificate refreshes are not subject to this limit. To eliminate the attack entirely, call m.SetHosts to enumerate the exact set of hosts that are allowed in certificate requests. Web Servers The basic requirement for use of a Manager is that there be an HTTPS server running on port 443 and calling m.GetCertificate to obtain TLS certificates. Using standard primitives, the way to do this is: srv := &http.Server{ Addr: ":https", TLSConfig: &tls.Config{ GetCertificate: m.GetCertificate, }, } srv.ListenAndServeTLS("", "") However, this pattern of serving HTTPS with demand-loaded TLS certificates comes up enough to wrap into a single method m.ServeHTTPS. Similarly, many HTTPS servers prefer to redirect HTTP clients to the HTTPS URLs. That functionality is provided by RedirectHTTP. The combination of serving HTTPS with demand-loaded TLS certificates and serving HTTPS redirects to HTTP clients is provided by m.Serve, as used in the original example above. func RedirectHTTP(w http.ResponseWriter, r *http.Request) type Manager struct { ... } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/lets.go000066400000000000000000000533271313450123100240130ustar00rootroot00000000000000// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package letsencrypt obtains TLS certificates from LetsEncrypt.org. // // LetsEncrypt.org is a service that issues free SSL/TLS certificates to servers // that can prove control over the given domain's DNS records or // the servers pointed at by those records. // // Quick Start // // A complete HTTP/HTTPS web server using TLS certificates from LetsEncrypt.org, // redirecting all HTTP access to HTTPS, and maintaining TLS certificates in a file // letsencrypt.cache across server restarts. // // package main // // import ( // "fmt" // "log" // "net/http" // "rsc.io/letsencrypt" // ) // // func main() { // http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // fmt.Fprintf(w, "Hello, TLS!\n") // }) // var m letsencrypt.Manager // if err := m.CacheFile("letsencrypt.cache"); err != nil { // log.Fatal(err) // } // log.Fatal(m.Serve()) // } // // Overview // // The fundamental type in this package is the Manager, which // manages obtaining and refreshing a collection of TLS certificates, // typically for use by an HTTPS server. // The example above shows the most basic use of a Manager. // The use can be customized by calling additional methods of the Manager. // // Registration // // A Manager m registers anonymously with LetsEncrypt.org, including agreeing to // the letsencrypt.org terms of service, the first time it needs to obtain a certificate. // To register with a particular email address and with the option of a // prompt for agreement with the terms of service, call m.Register. // // GetCertificate // // The Manager's GetCertificate method returns certificates // from the Manager's cache, filling the cache by requesting certificates // from LetsEncrypt.org. In this way, a server with a tls.Config.GetCertificate // set to m.GetCertificate will demand load a certificate for any host name // it serves. To force loading of certificates ahead of time, install m.GetCertificate // as before but then call m.Cert for each host name. // // A Manager can only obtain a certificate for a given host name if it can prove // control of that host name to LetsEncrypt.org. By default it proves control by // answering an HTTPS-based challenge: when // the LetsEncrypt.org servers connect to the named host on port 443 (HTTPS), // the TLS SNI handshake must use m.GetCertificate to obtain a per-host certificate. // The most common way to satisfy this requirement is for the host name to // resolve to the IP address of a (single) computer running m.ServeHTTPS, // or at least running a Go TLS server with tls.Config.GetCertificate set to m.GetCertificate. // However, other configurations are possible. For example, a group of machines // could use an implementation of tls.Config.GetCertificate that cached // certificates but handled cache misses by making RPCs to a Manager m // on an elected leader machine. // // In typical usage, then, the setting of tls.Config.GetCertificate to m.GetCertificate // serves two purposes: it provides certificates to the TLS server for ordinary serving, // and it also answers challenges to prove ownership of the domains in order to // obtain those certificates. // // To force the loading of a certificate for a given host into the Manager's cache, // use m.Cert. // // Persistent Storage // // If a server always starts with a zero Manager m, the server effectively fetches // a new certificate for each of its host name from LetsEncrypt.org on each restart. // This is unfortunate both because the server cannot start if LetsEncrypt.org is // unavailable and because LetsEncrypt.org limits how often it will issue a certificate // for a given host name (at time of writing, the limit is 5 per week for a given host name). // To save server state proactively to a cache file and to reload the server state from // that same file when creating a new manager, call m.CacheFile with the name of // the file to use. // // For alternate storage uses, m.Marshal returns the current state of the Manager // as an opaque string, m.Unmarshal sets the state of the Manager using a string // previously returned by m.Marshal (usually a different m), and m.Watch returns // a channel that receives notifications about state changes. // // Limits // // To avoid hitting basic rate limits on LetsEncrypt.org, a given Manager limits all its // interactions to at most one request every minute, with an initial allowed burst of // 20 requests. // // By default, if GetCertificate is asked for a certificate it does not have, it will in turn // ask LetsEncrypt.org for that certificate. This opens a potential attack where attackers // connect to a server by IP address and pretend to be asking for an incorrect host name. // Then GetCertificate will attempt to obtain a certificate for that host, incorrectly, // eventually hitting LetsEncrypt.org's rate limit for certificate requests and making it // impossible to obtain actual certificates. Because servers hold certificates for months // at a time, however, an attack would need to be sustained over a time period // of at least a month in order to cause real problems. // // To mitigate this kind of attack, a given Manager limits // itself to an average of one certificate request for a new host every three hours, // with an initial allowed burst of up to 20 requests. // Long-running servers will therefore stay // within the LetsEncrypt.org limit of 300 failed requests per month. // Certificate refreshes are not subject to this limit. // // To eliminate the attack entirely, call m.SetHosts to enumerate the exact set // of hosts that are allowed in certificate requests. // // Web Servers // // The basic requirement for use of a Manager is that there be an HTTPS server // running on port 443 and calling m.GetCertificate to obtain TLS certificates. // Using standard primitives, the way to do this is: // // srv := &http.Server{ // Addr: ":https", // TLSConfig: &tls.Config{ // GetCertificate: m.GetCertificate, // }, // } // srv.ListenAndServeTLS("", "") // // However, this pattern of serving HTTPS with demand-loaded TLS certificates // comes up enough to wrap into a single method m.ServeHTTPS. // // Similarly, many HTTPS servers prefer to redirect HTTP clients to the HTTPS URLs. // That functionality is provided by RedirectHTTP. // // The combination of serving HTTPS with demand-loaded TLS certificates and // serving HTTPS redirects to HTTP clients is provided by m.Serve, as used in // the original example above. // package letsencrypt import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/tls" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "io/ioutil" "log" "net" "net/http" "os" "strings" "sync" "time" "golang.org/x/net/context" "golang.org/x/time/rate" "github.com/xenolf/lego/acme" ) const letsEncryptURL = "https://acme-v01.api.letsencrypt.org/directory" const debug = false // A Manager m takes care of obtaining and refreshing a collection of TLS certificates // obtained by LetsEncrypt.org. // The zero Manager is not yet registered with LetsEncrypt.org and has no TLS certificates // but is nonetheless ready for use. // See the package comment for an overview of how to use a Manager. type Manager struct { mu sync.Mutex state state rateLimit *rate.Limiter newHostLimit *rate.Limiter certCache map[string]*cacheEntry certTokens map[string]*tls.Certificate watchChan chan struct{} } // Serve runs an HTTP/HTTPS web server using TLS certificates obtained by the manager. // The HTTP server redirects all requests to the HTTPS server. // The HTTPS server obtains TLS certificates as needed and responds to requests // by invoking http.DefaultServeMux. // // Serve does not return unitil the HTTPS server fails to start or else stops. // Either way, Serve can only return a non-nil error, never nil. func (m *Manager) Serve() error { l, err := net.Listen("tcp", ":http") if err != nil { return err } defer l.Close() go http.Serve(l, http.HandlerFunc(RedirectHTTP)) return m.ServeHTTPS() } // ServeHTTPS runs an HTTPS web server using TLS certificates obtained by the manager. // The HTTPS server obtains TLS certificates as needed and responds to requests // by invoking http.DefaultServeMux. // ServeHTTPS does not return unitil the HTTPS server fails to start or else stops. // Either way, ServeHTTPS can only return a non-nil error, never nil. func (m *Manager) ServeHTTPS() error { srv := &http.Server{ Addr: ":https", TLSConfig: &tls.Config{ GetCertificate: m.GetCertificate, }, } return srv.ListenAndServeTLS("", "") } // RedirectHTTP is an HTTP handler (suitable for use with http.HandleFunc) // that responds to all requests by redirecting to the same URL served over HTTPS. // It should only be invoked for requests received over HTTP. func RedirectHTTP(w http.ResponseWriter, r *http.Request) { if r.TLS != nil || r.Host == "" { http.Error(w, "not found", 404) } u := r.URL u.Host = r.Host u.Scheme = "https" http.Redirect(w, r, u.String(), 302) } // state is the serializable state for the Manager. // It also implements acme.User. type state struct { Email string Reg *acme.RegistrationResource Key string key *ecdsa.PrivateKey Hosts []string Certs map[string]stateCert } func (s *state) GetEmail() string { return s.Email } func (s *state) GetRegistration() *acme.RegistrationResource { return s.Reg } func (s *state) GetPrivateKey() crypto.PrivateKey { return s.key } type stateCert struct { Cert string Key string } func (cert stateCert) toTLS() (*tls.Certificate, error) { c, err := tls.X509KeyPair([]byte(cert.Cert), []byte(cert.Key)) if err != nil { return nil, err } return &c, err } type cacheEntry struct { host string m *Manager mu sync.Mutex cert *tls.Certificate timeout time.Time refreshing bool err error } func (m *Manager) init() { m.mu.Lock() if m.certCache == nil { m.rateLimit = rate.NewLimiter(rate.Every(1*time.Minute), 20) m.newHostLimit = rate.NewLimiter(rate.Every(3*time.Hour), 20) m.certCache = map[string]*cacheEntry{} m.certTokens = map[string]*tls.Certificate{} m.watchChan = make(chan struct{}, 1) m.watchChan <- struct{}{} } m.mu.Unlock() } // Watch returns the manager's watch channel, // which delivers a notification after every time the // manager's state (as exposed by Marshal and Unmarshal) changes. // All calls to Watch return the same watch channel. // // The watch channel includes notifications about changes // before the first call to Watch, so that in the pattern below, // the range loop executes once immediately, saving // the result of setup (along with any background updates that // may have raced in quickly). // // m := new(letsencrypt.Manager) // setup(m) // go backgroundUpdates(m) // for range m.Watch() { // save(m.Marshal()) // } // func (m *Manager) Watch() <-chan struct{} { m.init() m.updated() return m.watchChan } func (m *Manager) updated() { select { case m.watchChan <- struct{}{}: default: } } func (m *Manager) CacheFile(name string) error { f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE, 0600) if err != nil { return err } f.Close() data, err := ioutil.ReadFile(name) if err != nil { return err } if len(data) > 0 { if err := m.Unmarshal(string(data)); err != nil { return err } } go func() { for range m.Watch() { err := ioutil.WriteFile(name, []byte(m.Marshal()), 0600) if err != nil { log.Printf("writing letsencrypt cache: %v", err) } } }() return nil } // Registered reports whether the manager has registered with letsencrypt.org yet. func (m *Manager) Registered() bool { m.init() m.mu.Lock() defer m.mu.Unlock() return m.registered() } func (m *Manager) registered() bool { return m.state.Reg != nil && m.state.Reg.Body.Agreement != "" } // Register registers the manager with letsencrypt.org, using the given email address. // Registration may require agreeing to the letsencrypt.org terms of service. // If so, Register calls prompt(url) where url is the URL of the terms of service. // Prompt should report whether the caller agrees to the terms. // A nil prompt func is taken to mean that the user always agrees. // The email address is sent to LetsEncrypt.org but otherwise unchecked; // it can be omitted by passing the empty string. // // Calling Register is only required to make sure registration uses a // particular email address or to insert an explicit prompt into the // registration sequence. If the manager is not registered, it will // automatically register with no email address and automatic // agreement to the terms of service at the first call to Cert or GetCertificate. func (m *Manager) Register(email string, prompt func(string) bool) error { m.init() m.mu.Lock() defer m.mu.Unlock() return m.register(email, prompt) } func (m *Manager) register(email string, prompt func(string) bool) error { if m.registered() { return fmt.Errorf("already registered") } m.state.Email = email if m.state.key == nil { key, err := newKey() if err != nil { return fmt.Errorf("generating key: %v", err) } Key, err := marshalKey(key) if err != nil { return fmt.Errorf("generating key: %v", err) } m.state.key = key m.state.Key = string(Key) } c, err := acme.NewClient(letsEncryptURL, &m.state, acme.EC256) if err != nil { return fmt.Errorf("create client: %v", err) } reg, err := c.Register() if err != nil { return fmt.Errorf("register: %v", err) } m.state.Reg = reg if reg.Body.Agreement == "" { if prompt != nil && !prompt(reg.TosURL) { return fmt.Errorf("did not agree to TOS") } if err := c.AgreeToTOS(); err != nil { return fmt.Errorf("agreeing to TOS: %v", err) } } m.updated() return nil } // Marshal returns an encoding of the manager's state, // suitable for writing to disk and reloading by calling Unmarshal. // The state includes registration status, the configured host list // from SetHosts, and all known certificates, including their private // cryptographic keys. // Consequently, the state should be kept private. func (m *Manager) Marshal() string { m.init() js, err := json.MarshalIndent(&m.state, "", "\t") if err != nil { panic("unexpected json.Marshal failure") } return string(js) } // Unmarshal restores the state encoded by a previous call to Marshal // (perhaps on a different Manager in a different program). func (m *Manager) Unmarshal(enc string) error { m.init() var st state if err := json.Unmarshal([]byte(enc), &st); err != nil { return err } if st.Key != "" { key, err := unmarshalKey(st.Key) if err != nil { return err } st.key = key } m.state = st for host, cert := range m.state.Certs { c, err := cert.toTLS() if err != nil { log.Printf("letsencrypt: ignoring entry for %s: %v", host, err) continue } m.certCache[host] = &cacheEntry{host: host, m: m, cert: c} } m.updated() return nil } // SetHosts sets the manager's list of known host names. // If the list is non-nil, the manager will only ever attempt to acquire // certificates for host names on the list. // If the list is nil, the manager does not restrict the hosts it will // ask for certificates for. func (m *Manager) SetHosts(hosts []string) { m.init() m.mu.Lock() m.state.Hosts = append(m.state.Hosts[:0], hosts...) m.mu.Unlock() m.updated() } // GetCertificate can be placed a tls.Config's GetCertificate field to make // the TLS server use Let's Encrypt certificates. // Each time a client connects to the TLS server expecting a new host name, // the TLS server's call to GetCertificate will trigger an exchange with the // Let's Encrypt servers to obtain that certificate, subject to the manager rate limits. // // As noted in the Manager's documentation comment, // to obtain a certificate for a given host name, that name // must resolve to a computer running a TLS server on port 443 // that obtains TLS SNI certificates by calling m.GetCertificate. // In the standard usage, then, installing m.GetCertificate in the tls.Config // both automatically provisions the TLS certificates needed for // ordinary HTTPS service and answers the challenges from LetsEncrypt.org. func (m *Manager) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { m.init() host := clientHello.ServerName if debug { log.Printf("GetCertificate %s", host) } if strings.HasSuffix(host, ".acme.invalid") { m.mu.Lock() cert := m.certTokens[host] m.mu.Unlock() if cert == nil { return nil, fmt.Errorf("unknown host") } return cert, nil } return m.Cert(host) } // Cert returns the certificate for the given host name, obtaining a new one if necessary. // // As noted in the documentation for Manager and for the GetCertificate method, // obtaining a certificate requires that m.GetCertificate be associated with host. // In most servers, simply starting a TLS server with a configuration referring // to m.GetCertificate is sufficient, and Cert need not be called. // // The main use of Cert is to force the manager to obtain a certificate // for a particular host name ahead of time. func (m *Manager) Cert(host string) (*tls.Certificate, error) { host = strings.ToLower(host) if debug { log.Printf("Cert %s", host) } m.init() m.mu.Lock() if !m.registered() { m.register("", nil) } ok := false if m.state.Hosts == nil { ok = true } else { for _, h := range m.state.Hosts { if host == h { ok = true break } } } if !ok { m.mu.Unlock() return nil, fmt.Errorf("unknown host") } // Otherwise look in our cert cache. entry, ok := m.certCache[host] if !ok { r := m.rateLimit.Reserve() ok := r.OK() if ok { ok = m.newHostLimit.Allow() if !ok { r.Cancel() } } if !ok { m.mu.Unlock() return nil, fmt.Errorf("rate limited") } entry = &cacheEntry{host: host, m: m} m.certCache[host] = entry } m.mu.Unlock() entry.mu.Lock() defer entry.mu.Unlock() entry.init() if entry.err != nil { return nil, entry.err } return entry.cert, nil } func (e *cacheEntry) init() { if e.err != nil && time.Now().Before(e.timeout) { return } if e.cert != nil { if e.timeout.IsZero() { t, err := certRefreshTime(e.cert) if err != nil { e.err = err e.timeout = time.Now().Add(1 * time.Minute) e.cert = nil return } e.timeout = t } if time.Now().After(e.timeout) && !e.refreshing { e.refreshing = true go e.refresh() } return } cert, refreshTime, err := e.m.verify(e.host) e.m.mu.Lock() e.m.certCache[e.host] = e e.m.mu.Unlock() e.install(cert, refreshTime, err) } func (e *cacheEntry) install(cert *tls.Certificate, refreshTime time.Time, err error) { e.cert = nil e.timeout = time.Time{} e.err = nil if err != nil { e.err = err e.timeout = time.Now().Add(1 * time.Minute) return } e.cert = cert e.timeout = refreshTime } func (e *cacheEntry) refresh() { e.m.rateLimit.Wait(context.Background()) cert, refreshTime, err := e.m.verify(e.host) e.mu.Lock() defer e.mu.Unlock() e.refreshing = false if err == nil { e.install(cert, refreshTime, nil) } } func (m *Manager) verify(host string) (cert *tls.Certificate, refreshTime time.Time, err error) { c, err := acme.NewClient(letsEncryptURL, &m.state, acme.EC256) if err != nil { return } if err = c.SetChallengeProvider(acme.TLSSNI01, tlsProvider{m}); err != nil { return } c.SetChallengeProvider(acme.TLSSNI01, tlsProvider{m}) c.ExcludeChallenges([]acme.Challenge{acme.HTTP01}) acmeCert, errmap := c.ObtainCertificate([]string{host}, true, nil) if len(errmap) > 0 { if debug { log.Printf("ObtainCertificate %v => %v", host, errmap) } err = fmt.Errorf("%v", errmap) return } entryCert := stateCert{ Cert: string(acmeCert.Certificate), Key: string(acmeCert.PrivateKey), } cert, err = entryCert.toTLS() if err != nil { if debug { log.Printf("ObtainCertificate %v toTLS failure: %v", host, err) } err = err return } if refreshTime, err = certRefreshTime(cert); err != nil { return } m.mu.Lock() if m.state.Certs == nil { m.state.Certs = make(map[string]stateCert) } m.state.Certs[host] = entryCert m.mu.Unlock() m.updated() return cert, refreshTime, nil } func certRefreshTime(cert *tls.Certificate) (time.Time, error) { xc, err := x509.ParseCertificate(cert.Certificate[0]) if err != nil { if debug { log.Printf("ObtainCertificate to X.509 failure: %v", err) } return time.Time{}, err } t := xc.NotBefore.Add(xc.NotAfter.Sub(xc.NotBefore) / 2) monthEarly := xc.NotAfter.Add(-30 * 24 * time.Hour) if t.Before(monthEarly) { t = monthEarly } return t, nil } // tlsProvider implements acme.ChallengeProvider for TLS handshake challenges. type tlsProvider struct { m *Manager } func (p tlsProvider) Present(domain, token, keyAuth string) error { cert, dom, err := acme.TLSSNI01ChallengeCertDomain(keyAuth) if err != nil { return err } p.m.mu.Lock() p.m.certTokens[dom] = &cert p.m.mu.Unlock() return nil } func (p tlsProvider) CleanUp(domain, token, keyAuth string) error { _, dom, err := acme.TLSSNI01ChallengeCertDomain(keyAuth) if err != nil { return err } p.m.mu.Lock() delete(p.m.certTokens, dom) p.m.mu.Unlock() return nil } func marshalKey(key *ecdsa.PrivateKey) ([]byte, error) { data, err := x509.MarshalECPrivateKey(key) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: data}), nil } func unmarshalKey(text string) (*ecdsa.PrivateKey, error) { b, _ := pem.Decode([]byte(text)) if b == nil { return nil, fmt.Errorf("unmarshalKey: missing key") } if b.Type != "EC PRIVATE KEY" { return nil, fmt.Errorf("unmarshalKey: found %q, not %q", b.Type, "EC PRIVATE KEY") } k, err := x509.ParseECPrivateKey(b.Bytes) if err != nil { return nil, fmt.Errorf("unmarshalKey: %v", err) } return k, nil } func newKey() (*ecdsa.PrivateKey, error) { return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/000077500000000000000000000000001313450123100240005ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/000077500000000000000000000000001313450123100260375ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/000077500000000000000000000000001313450123100273325ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/000077500000000000000000000000001313450123100302605ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/LICENSE000066400000000000000000000020731313450123100312670ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Sebastian Erhart Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/000077500000000000000000000000001313450123100311655ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/challenges.go000066400000000000000000000015431313450123100336240ustar00rootroot00000000000000package acme // Challenge is a string that identifies a particular type and version of ACME challenge. type Challenge string const ( // HTTP01 is the "http-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#http // Note: HTTP01ChallengePath returns the URL path to fulfill this challenge HTTP01 = Challenge("http-01") // TLSSNI01 is the "tls-sni-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#tls-with-server-name-indication-tls-sni // Note: TLSSNI01ChallengeCert returns a certificate to fulfill this challenge TLSSNI01 = Challenge("tls-sni-01") // DNS01 is the "dns-01" ACME challenge https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md#dns // Note: DNS01Record returns a DNS record which will fulfill this challenge DNS01 = Challenge("dns-01") ) docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/client.go000066400000000000000000000473241313450123100330040ustar00rootroot00000000000000// Package acme implements the ACME protocol for Let's Encrypt and other conforming providers. package acme import ( "crypto" "crypto/x509" "encoding/base64" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net" "regexp" "strconv" "strings" "time" ) var ( // Logger is an optional custom logger. Logger *log.Logger ) // logf writes a log entry. It uses Logger if not // nil, otherwise it uses the default log.Logger. func logf(format string, args ...interface{}) { if Logger != nil { Logger.Printf(format, args...) } else { log.Printf(format, args...) } } // User interface is to be implemented by users of this library. // It is used by the client type to get user specific information. type User interface { GetEmail() string GetRegistration() *RegistrationResource GetPrivateKey() crypto.PrivateKey } // Interface for all challenge solvers to implement. type solver interface { Solve(challenge challenge, domain string) error } type validateFunc func(j *jws, domain, uri string, chlng challenge) error // Client is the user-friendy way to ACME type Client struct { directory directory user User jws *jws keyType KeyType issuerCert []byte solvers map[Challenge]solver } // NewClient creates a new ACME client on behalf of the user. The client will depend on // the ACME directory located at caDirURL for the rest of its actions. It will // generate private keys for certificates of size keyBits. func NewClient(caDirURL string, user User, keyType KeyType) (*Client, error) { privKey := user.GetPrivateKey() if privKey == nil { return nil, errors.New("private key was nil") } var dir directory if _, err := getJSON(caDirURL, &dir); err != nil { return nil, fmt.Errorf("get directory at '%s': %v", caDirURL, err) } if dir.NewRegURL == "" { return nil, errors.New("directory missing new registration URL") } if dir.NewAuthzURL == "" { return nil, errors.New("directory missing new authz URL") } if dir.NewCertURL == "" { return nil, errors.New("directory missing new certificate URL") } if dir.RevokeCertURL == "" { return nil, errors.New("directory missing revoke certificate URL") } jws := &jws{privKey: privKey, directoryURL: caDirURL} // REVIEW: best possibility? // Add all available solvers with the right index as per ACME // spec to this map. Otherwise they won`t be found. solvers := make(map[Challenge]solver) solvers[HTTP01] = &httpChallenge{jws: jws, validate: validate, provider: &HTTPProviderServer{}} solvers[TLSSNI01] = &tlsSNIChallenge{jws: jws, validate: validate, provider: &TLSProviderServer{}} return &Client{directory: dir, user: user, jws: jws, keyType: keyType, solvers: solvers}, nil } // SetChallengeProvider specifies a custom provider that will make the solution available func (c *Client) SetChallengeProvider(challenge Challenge, p ChallengeProvider) error { switch challenge { case HTTP01: c.solvers[challenge] = &httpChallenge{jws: c.jws, validate: validate, provider: p} case TLSSNI01: c.solvers[challenge] = &tlsSNIChallenge{jws: c.jws, validate: validate, provider: p} default: return fmt.Errorf("Unknown challenge %v", challenge) } return nil } // SetHTTPAddress specifies a custom interface:port to be used for HTTP based challenges. // If this option is not used, the default port 80 and all interfaces will be used. // To only specify a port and no interface use the ":port" notation. func (c *Client) SetHTTPAddress(iface string) error { host, port, err := net.SplitHostPort(iface) if err != nil { return err } if chlng, ok := c.solvers[HTTP01]; ok { chlng.(*httpChallenge).provider = NewHTTPProviderServer(host, port) } return nil } // SetTLSAddress specifies a custom interface:port to be used for TLS based challenges. // If this option is not used, the default port 443 and all interfaces will be used. // To only specify a port and no interface use the ":port" notation. func (c *Client) SetTLSAddress(iface string) error { host, port, err := net.SplitHostPort(iface) if err != nil { return err } if chlng, ok := c.solvers[TLSSNI01]; ok { chlng.(*tlsSNIChallenge).provider = NewTLSProviderServer(host, port) } return nil } // ExcludeChallenges explicitly removes challenges from the pool for solving. func (c *Client) ExcludeChallenges(challenges []Challenge) { // Loop through all challenges and delete the requested one if found. for _, challenge := range challenges { delete(c.solvers, challenge) } } // Register the current account to the ACME server. func (c *Client) Register() (*RegistrationResource, error) { if c == nil || c.user == nil { return nil, errors.New("acme: cannot register a nil client or user") } logf("[INFO] acme: Registering account for %s", c.user.GetEmail()) regMsg := registrationMessage{ Resource: "new-reg", } if c.user.GetEmail() != "" { regMsg.Contact = []string{"mailto:" + c.user.GetEmail()} } else { regMsg.Contact = []string{} } var serverReg Registration hdr, err := postJSON(c.jws, c.directory.NewRegURL, regMsg, &serverReg) if err != nil { return nil, err } reg := &RegistrationResource{Body: serverReg} links := parseLinks(hdr["Link"]) reg.URI = hdr.Get("Location") if links["terms-of-service"] != "" { reg.TosURL = links["terms-of-service"] } if links["next"] != "" { reg.NewAuthzURL = links["next"] } else { return nil, errors.New("acme: The server did not return 'next' link to proceed") } return reg, nil } // AgreeToTOS updates the Client registration and sends the agreement to // the server. func (c *Client) AgreeToTOS() error { reg := c.user.GetRegistration() reg.Body.Agreement = c.user.GetRegistration().TosURL reg.Body.Resource = "reg" _, err := postJSON(c.jws, c.user.GetRegistration().URI, c.user.GetRegistration().Body, nil) return err } // ObtainCertificate tries to obtain a single certificate using all domains passed into it. // The first domain in domains is used for the CommonName field of the certificate, all other // domains are added using the Subject Alternate Names extension. A new private key is generated // for every invocation of this function. If you do not want that you can supply your own private key // in the privKey parameter. If this parameter is non-nil it will be used instead of generating a new one. // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // This function will never return a partial certificate. If one domain in the list fails, // the whole certificate will fail. func (c *Client) ObtainCertificate(domains []string, bundle bool, privKey crypto.PrivateKey) (CertificateResource, map[string]error) { if bundle { logf("[INFO][%s] acme: Obtaining bundled SAN certificate", strings.Join(domains, ", ")) } else { logf("[INFO][%s] acme: Obtaining SAN certificate", strings.Join(domains, ", ")) } challenges, failures := c.getChallenges(domains) // If any challenge fails - return. Do not generate partial SAN certificates. if len(failures) > 0 { return CertificateResource{}, failures } errs := c.solveChallenges(challenges) // If any challenge fails - return. Do not generate partial SAN certificates. if len(errs) > 0 { return CertificateResource{}, errs } logf("[INFO][%s] acme: Validations succeeded; requesting certificates", strings.Join(domains, ", ")) cert, err := c.requestCertificate(challenges, bundle, privKey) if err != nil { for _, chln := range challenges { failures[chln.Domain] = err } } return cert, failures } // RevokeCertificate takes a PEM encoded certificate or bundle and tries to revoke it at the CA. func (c *Client) RevokeCertificate(certificate []byte) error { certificates, err := parsePEMBundle(certificate) if err != nil { return err } x509Cert := certificates[0] if x509Cert.IsCA { return fmt.Errorf("Certificate bundle starts with a CA certificate") } encodedCert := base64.URLEncoding.EncodeToString(x509Cert.Raw) _, err = postJSON(c.jws, c.directory.RevokeCertURL, revokeCertMessage{Resource: "revoke-cert", Certificate: encodedCert}, nil) return err } // RenewCertificate takes a CertificateResource and tries to renew the certificate. // If the renewal process succeeds, the new certificate will ge returned in a new CertResource. // Please be aware that this function will return a new certificate in ANY case that is not an error. // If the server does not provide us with a new cert on a GET request to the CertURL // this function will start a new-cert flow where a new certificate gets generated. // If bundle is true, the []byte contains both the issuer certificate and // your issued certificate as a bundle. // For private key reuse the PrivateKey property of the passed in CertificateResource should be non-nil. func (c *Client) RenewCertificate(cert CertificateResource, bundle bool) (CertificateResource, error) { // Input certificate is PEM encoded. Decode it here as we may need the decoded // cert later on in the renewal process. The input may be a bundle or a single certificate. certificates, err := parsePEMBundle(cert.Certificate) if err != nil { return CertificateResource{}, err } x509Cert := certificates[0] if x509Cert.IsCA { return CertificateResource{}, fmt.Errorf("[%s] Certificate bundle starts with a CA certificate", cert.Domain) } // This is just meant to be informal for the user. timeLeft := x509Cert.NotAfter.Sub(time.Now().UTC()) logf("[INFO][%s] acme: Trying renewal with %d hours remaining", cert.Domain, int(timeLeft.Hours())) // The first step of renewal is to check if we get a renewed cert // directly from the cert URL. resp, err := httpGet(cert.CertURL) if err != nil { return CertificateResource{}, err } defer resp.Body.Close() serverCertBytes, err := ioutil.ReadAll(resp.Body) if err != nil { return CertificateResource{}, err } serverCert, err := x509.ParseCertificate(serverCertBytes) if err != nil { return CertificateResource{}, err } // If the server responds with a different certificate we are effectively renewed. // TODO: Further test if we can actually use the new certificate (Our private key works) if !x509Cert.Equal(serverCert) { logf("[INFO][%s] acme: Server responded with renewed certificate", cert.Domain) issuedCert := pemEncode(derCertificateBytes(serverCertBytes)) // If bundle is true, we want to return a certificate bundle. // To do this, we need the issuer certificate. if bundle { // The issuer certificate link is always supplied via an "up" link // in the response headers of a new certificate. links := parseLinks(resp.Header["Link"]) issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { // If we fail to acquire the issuer cert, return the issued certificate - do not fail. logf("[ERROR][%s] acme: Could not bundle issuer certificate: %v", cert.Domain, err) } else { // Success - append the issuer cert to the issued cert. issuerCert = pemEncode(derCertificateBytes(issuerCert)) issuedCert = append(issuedCert, issuerCert...) } } cert.Certificate = issuedCert return cert, nil } var privKey crypto.PrivateKey if cert.PrivateKey != nil { privKey, err = parsePEMPrivateKey(cert.PrivateKey) if err != nil { return CertificateResource{}, err } } var domains []string var failures map[string]error // check for SAN certificate if len(x509Cert.DNSNames) > 1 { domains = append(domains, x509Cert.Subject.CommonName) for _, sanDomain := range x509Cert.DNSNames { if sanDomain == x509Cert.Subject.CommonName { continue } domains = append(domains, sanDomain) } } else { domains = append(domains, x509Cert.Subject.CommonName) } newCert, failures := c.ObtainCertificate(domains, bundle, privKey) return newCert, failures[cert.Domain] } // Looks through the challenge combinations to find a solvable match. // Then solves the challenges in series and returns. func (c *Client) solveChallenges(challenges []authorizationResource) map[string]error { // loop through the resources, basically through the domains. failures := make(map[string]error) for _, authz := range challenges { // no solvers - no solving if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil { for i, solver := range solvers { // TODO: do not immediately fail if one domain fails to validate. err := solver.Solve(authz.Body.Challenges[i], authz.Domain) if err != nil { failures[authz.Domain] = err } } } else { failures[authz.Domain] = fmt.Errorf("[%s] acme: Could not determine solvers", authz.Domain) } } return failures } // Checks all combinations from the server and returns an array of // solvers which should get executed in series. func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver { for _, combination := range auth.Combinations { solvers := make(map[int]solver) for _, idx := range combination { if solver, ok := c.solvers[auth.Challenges[idx].Type]; ok { solvers[idx] = solver } else { logf("[INFO][%s] acme: Could not find solver for: %s", domain, auth.Challenges[idx].Type) } } // If we can solve the whole combination, return the solvers if len(solvers) == len(combination) { return solvers } } return nil } // Get the challenges needed to proof our identifier to the ACME server. func (c *Client) getChallenges(domains []string) ([]authorizationResource, map[string]error) { resc, errc := make(chan authorizationResource), make(chan domainError) for _, domain := range domains { go func(domain string) { authMsg := authorization{Resource: "new-authz", Identifier: identifier{Type: "dns", Value: domain}} var authz authorization hdr, err := postJSON(c.jws, c.user.GetRegistration().NewAuthzURL, authMsg, &authz) if err != nil { errc <- domainError{Domain: domain, Error: err} return } links := parseLinks(hdr["Link"]) if links["next"] == "" { logf("[ERROR][%s] acme: Server did not provide next link to proceed", domain) return } resc <- authorizationResource{Body: authz, NewCertURL: links["next"], AuthURL: hdr.Get("Location"), Domain: domain} }(domain) } responses := make(map[string]authorizationResource) failures := make(map[string]error) for i := 0; i < len(domains); i++ { select { case res := <-resc: responses[res.Domain] = res case err := <-errc: failures[err.Domain] = err.Error } } challenges := make([]authorizationResource, 0, len(responses)) for _, domain := range domains { if challenge, ok := responses[domain]; ok { challenges = append(challenges, challenge) } } close(resc) close(errc) return challenges, failures } func (c *Client) requestCertificate(authz []authorizationResource, bundle bool, privKey crypto.PrivateKey) (CertificateResource, error) { if len(authz) == 0 { return CertificateResource{}, errors.New("Passed no authorizations to requestCertificate!") } commonName := authz[0] var err error if privKey == nil { privKey, err = generatePrivateKey(c.keyType) if err != nil { return CertificateResource{}, err } } var san []string var authURLs []string for _, auth := range authz[1:] { san = append(san, auth.Domain) authURLs = append(authURLs, auth.AuthURL) } // TODO: should the CSR be customizable? csr, err := generateCsr(privKey, commonName.Domain, san) if err != nil { return CertificateResource{}, err } csrString := base64.URLEncoding.EncodeToString(csr) jsonBytes, err := json.Marshal(csrMessage{Resource: "new-cert", Csr: csrString, Authorizations: authURLs}) if err != nil { return CertificateResource{}, err } resp, err := c.jws.post(commonName.NewCertURL, jsonBytes) if err != nil { return CertificateResource{}, err } privateKeyPem := pemEncode(privKey) cerRes := CertificateResource{ Domain: commonName.Domain, CertURL: resp.Header.Get("Location"), PrivateKey: privateKeyPem} for { switch resp.StatusCode { case 201, 202: cert, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) resp.Body.Close() if err != nil { return CertificateResource{}, err } // The server returns a body with a length of zero if the // certificate was not ready at the time this request completed. // Otherwise the body is the certificate. if len(cert) > 0 { cerRes.CertStableURL = resp.Header.Get("Content-Location") cerRes.AccountRef = c.user.GetRegistration().URI issuedCert := pemEncode(derCertificateBytes(cert)) // If bundle is true, we want to return a certificate bundle. // To do this, we need the issuer certificate. if bundle { // The issuer certificate link is always supplied via an "up" link // in the response headers of a new certificate. links := parseLinks(resp.Header["Link"]) issuerCert, err := c.getIssuerCertificate(links["up"]) if err != nil { // If we fail to acquire the issuer cert, return the issued certificate - do not fail. logf("[WARNING][%s] acme: Could not bundle issuer certificate: %v", commonName.Domain, err) } else { // Success - append the issuer cert to the issued cert. issuerCert = pemEncode(derCertificateBytes(issuerCert)) issuedCert = append(issuedCert, issuerCert...) } } cerRes.Certificate = issuedCert logf("[INFO][%s] Server responded with a certificate.", commonName.Domain) return cerRes, nil } // The certificate was granted but is not yet issued. // Check retry-after and loop. ra := resp.Header.Get("Retry-After") retryAfter, err := strconv.Atoi(ra) if err != nil { return CertificateResource{}, err } logf("[INFO][%s] acme: Server responded with status 202; retrying after %ds", commonName.Domain, retryAfter) time.Sleep(time.Duration(retryAfter) * time.Second) break default: return CertificateResource{}, handleHTTPError(resp) } resp, err = httpGet(cerRes.CertURL) if err != nil { return CertificateResource{}, err } } } // getIssuerCertificate requests the issuer certificate and caches it for // subsequent requests. func (c *Client) getIssuerCertificate(url string) ([]byte, error) { logf("[INFO] acme: Requesting issuer cert from %s", url) if c.issuerCert != nil { return c.issuerCert, nil } resp, err := httpGet(url) if err != nil { return nil, err } defer resp.Body.Close() issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) if err != nil { return nil, err } _, err = x509.ParseCertificate(issuerBytes) if err != nil { return nil, err } c.issuerCert = issuerBytes return issuerBytes, err } func parseLinks(links []string) map[string]string { aBrkt := regexp.MustCompile("[<>]") slver := regexp.MustCompile("(.+) *= *\"(.+)\"") linkMap := make(map[string]string) for _, link := range links { link = aBrkt.ReplaceAllString(link, "") parts := strings.Split(link, ";") matches := slver.FindStringSubmatch(parts[1]) if len(matches) > 0 { linkMap[matches[2]] = parts[0] } } return linkMap } // validate makes the ACME server start validating a // challenge response, only returning once it is done. func validate(j *jws, domain, uri string, chlng challenge) error { var challengeResponse challenge hdr, err := postJSON(j, uri, chlng, &challengeResponse) if err != nil { return err } // After the path is sent, the ACME server will access our server. // Repeatedly check the server for an updated status on our request. for { switch challengeResponse.Status { case "valid": logf("[INFO][%s] The server validated our request", domain) return nil case "pending": break case "invalid": return handleChallengeError(challengeResponse) default: return errors.New("The server returned an unexpected state.") } ra, err := strconv.Atoi(hdr.Get("Retry-After")) if err != nil { // The ACME server MUST return a Retry-After. // If it doesn't, we'll just poll hard. ra = 1 } time.Sleep(time.Duration(ra) * time.Second) hdr, err = getJSON(uri, &challengeResponse) if err != nil { return err } } } client_test.go000066400000000000000000000143311313450123100337540ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "crypto" "crypto/rand" "crypto/rsa" "encoding/json" "net" "net/http" "net/http/httptest" "strings" "testing" ) func TestNewClient(t *testing.T) { keyBits := 32 // small value keeps test fast keyType := RSA2048 key, err := rsa.GenerateKey(rand.Reader, keyBits) if err != nil { t.Fatal("Could not generate test key:", err) } user := mockUser{ email: "test@test.com", regres: new(RegistrationResource), privatekey: key, } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) w.Write(data) })) client, err := NewClient(ts.URL, user, keyType) if err != nil { t.Fatalf("Could not create client: %v", err) } if client.jws == nil { t.Fatalf("Expected client.jws to not be nil") } if expected, actual := key, client.jws.privKey; actual != expected { t.Errorf("Expected jws.privKey to be %p but was %p", expected, actual) } if client.keyType != keyType { t.Errorf("Expected keyType to be %s but was %s", keyType, client.keyType) } if expected, actual := 2, len(client.solvers); actual != expected { t.Fatalf("Expected %d solver(s), got %d", expected, actual) } } func TestClientOptPort(t *testing.T) { keyBits := 32 // small value keeps test fast key, err := rsa.GenerateKey(rand.Reader, keyBits) if err != nil { t.Fatal("Could not generate test key:", err) } user := mockUser{ email: "test@test.com", regres: new(RegistrationResource), privatekey: key, } ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := json.Marshal(directory{NewAuthzURL: "http://test", NewCertURL: "http://test", NewRegURL: "http://test", RevokeCertURL: "http://test"}) w.Write(data) })) optPort := "1234" optHost := "" client, err := NewClient(ts.URL, user, RSA2048) if err != nil { t.Fatalf("Could not create client: %v", err) } client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) httpSolver, ok := client.solvers[HTTP01].(*httpChallenge) if !ok { t.Fatal("Expected http-01 solver to be httpChallenge type") } if httpSolver.jws != client.jws { t.Error("Expected http-01 to have same jws as client") } if got := httpSolver.provider.(*HTTPProviderServer).port; got != optPort { t.Errorf("Expected http-01 to have port %s but was %s", optPort, got) } if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } httpsSolver, ok := client.solvers[TLSSNI01].(*tlsSNIChallenge) if !ok { t.Fatal("Expected tls-sni-01 solver to be httpChallenge type") } if httpsSolver.jws != client.jws { t.Error("Expected tls-sni-01 to have same jws as client") } if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) } if got := httpsSolver.provider.(*TLSProviderServer).iface; got != optHost { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optHost, got) } // test setting different host optHost = "127.0.0.1" client.SetHTTPAddress(net.JoinHostPort(optHost, optPort)) client.SetTLSAddress(net.JoinHostPort(optHost, optPort)) if got := httpSolver.provider.(*HTTPProviderServer).iface; got != optHost { t.Errorf("Expected http-01 to have iface %s but was %s", optHost, got) } if got := httpsSolver.provider.(*TLSProviderServer).port; got != optPort { t.Errorf("Expected tls-sni-01 to have port %s but was %s", optPort, got) } } func TestValidate(t *testing.T) { var statuses []string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Minimal stub ACME server for validation. w.Header().Add("Replay-Nonce", "12345") w.Header().Add("Retry-After", "0") switch r.Method { case "HEAD": case "POST": st := statuses[0] statuses = statuses[1:] writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) case "GET": st := statuses[0] statuses = statuses[1:] writeJSONResponse(w, &challenge{Type: "http-01", Status: st, URI: "http://example.com/", Token: "token"}) default: http.Error(w, r.Method, http.StatusMethodNotAllowed) } })) defer ts.Close() privKey, _ := rsa.GenerateKey(rand.Reader, 512) j := &jws{privKey: privKey, directoryURL: ts.URL} tsts := []struct { name string statuses []string want string }{ {"POST-unexpected", []string{"weird"}, "unexpected"}, {"POST-valid", []string{"valid"}, ""}, {"POST-invalid", []string{"invalid"}, "Error Detail"}, {"GET-unexpected", []string{"pending", "weird"}, "unexpected"}, {"GET-valid", []string{"pending", "valid"}, ""}, {"GET-invalid", []string{"pending", "invalid"}, "Error Detail"}, } for _, tst := range tsts { statuses = tst.statuses if err := validate(j, "example.com", ts.URL, challenge{Type: "http-01", Token: "token"}); err == nil && tst.want != "" { t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want) } else if err != nil && !strings.Contains(err.Error(), tst.want) { t.Errorf("[%s] validate: got error %v, want something with %q", tst.name, err, tst.want) } } } // writeJSONResponse marshals the body as JSON and writes it to the response. func writeJSONResponse(w http.ResponseWriter, body interface{}) { bs, err := json.Marshal(body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") if _, err := w.Write(bs); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } } // stubValidate is like validate, except it does nothing. func stubValidate(j *jws, domain, uri string, chlng challenge) error { return nil } type mockUser struct { email string regres *RegistrationResource privatekey *rsa.PrivateKey } func (u mockUser) GetEmail() string { return u.email } func (u mockUser) GetRegistration() *RegistrationResource { return u.regres } func (u mockUser) GetPrivateKey() crypto.PrivateKey { return u.privatekey } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/crypto.go000066400000000000000000000211701313450123100330350ustar00rootroot00000000000000package acme import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/base64" "encoding/pem" "errors" "fmt" "io" "io/ioutil" "math/big" "net/http" "strings" "time" "golang.org/x/crypto/ocsp" ) // KeyType represents the key algo as well as the key size or curve to use. type KeyType string type derCertificateBytes []byte // Constants for all key types we support. const ( EC256 = KeyType("P256") EC384 = KeyType("P384") RSA2048 = KeyType("2048") RSA4096 = KeyType("4096") RSA8192 = KeyType("8192") ) const ( // OCSPGood means that the certificate is valid. OCSPGood = ocsp.Good // OCSPRevoked means that the certificate has been deliberately revoked. OCSPRevoked = ocsp.Revoked // OCSPUnknown means that the OCSP responder doesn't know about the certificate. OCSPUnknown = ocsp.Unknown // OCSPServerFailed means that the OCSP responder failed to process the request. OCSPServerFailed = ocsp.ServerFailed ) // GetOCSPForCert takes a PEM encoded cert or cert bundle returning the raw OCSP response, // the parsed response, and an error, if any. The returned []byte can be passed directly // into the OCSPStaple property of a tls.Certificate. If the bundle only contains the // issued certificate, this function will try to get the issuer certificate from the // IssuingCertificateURL in the certificate. If the []byte and/or ocsp.Response return // values are nil, the OCSP status may be assumed OCSPUnknown. func GetOCSPForCert(bundle []byte) ([]byte, *ocsp.Response, error) { certificates, err := parsePEMBundle(bundle) if err != nil { return nil, nil, err } // We expect the certificate slice to be ordered downwards the chain. // SRV CRT -> CA. We need to pull the leaf and issuer certs out of it, // which should always be the first two certificates. If there's no // OCSP server listed in the leaf cert, there's nothing to do. And if // we have only one certificate so far, we need to get the issuer cert. issuedCert := certificates[0] if len(issuedCert.OCSPServer) == 0 { return nil, nil, errors.New("no OCSP server specified in cert") } if len(certificates) == 1 { // TODO: build fallback. If this fails, check the remaining array entries. if len(issuedCert.IssuingCertificateURL) == 0 { return nil, nil, errors.New("no issuing certificate URL") } resp, err := httpGet(issuedCert.IssuingCertificateURL[0]) if err != nil { return nil, nil, err } defer resp.Body.Close() issuerBytes, err := ioutil.ReadAll(limitReader(resp.Body, 1024*1024)) if err != nil { return nil, nil, err } issuerCert, err := x509.ParseCertificate(issuerBytes) if err != nil { return nil, nil, err } // Insert it into the slice on position 0 // We want it ordered right SRV CRT -> CA certificates = append(certificates, issuerCert) } issuerCert := certificates[1] // Finally kick off the OCSP request. ocspReq, err := ocsp.CreateRequest(issuedCert, issuerCert, nil) if err != nil { return nil, nil, err } reader := bytes.NewReader(ocspReq) req, err := httpPost(issuedCert.OCSPServer[0], "application/ocsp-request", reader) if err != nil { return nil, nil, err } defer req.Body.Close() ocspResBytes, err := ioutil.ReadAll(limitReader(req.Body, 1024*1024)) ocspRes, err := ocsp.ParseResponse(ocspResBytes, issuerCert) if err != nil { return nil, nil, err } if ocspRes.Certificate == nil { err = ocspRes.CheckSignatureFrom(issuerCert) if err != nil { return nil, nil, err } } return ocspResBytes, ocspRes, nil } func getKeyAuthorization(token string, key interface{}) (string, error) { var publicKey crypto.PublicKey switch k := key.(type) { case *ecdsa.PrivateKey: publicKey = k.Public() case *rsa.PrivateKey: publicKey = k.Public() } // Generate the Key Authorization for the challenge jwk := keyAsJWK(publicKey) if jwk == nil { return "", errors.New("Could not generate JWK from key.") } thumbBytes, err := jwk.Thumbprint(crypto.SHA256) if err != nil { return "", err } // unpad the base64URL keyThumb := base64.URLEncoding.EncodeToString(thumbBytes) index := strings.Index(keyThumb, "=") if index != -1 { keyThumb = keyThumb[:index] } return token + "." + keyThumb, nil } // parsePEMBundle parses a certificate bundle from top to bottom and returns // a slice of x509 certificates. This function will error if no certificates are found. func parsePEMBundle(bundle []byte) ([]*x509.Certificate, error) { var certificates []*x509.Certificate var certDERBlock *pem.Block for { certDERBlock, bundle = pem.Decode(bundle) if certDERBlock == nil { break } if certDERBlock.Type == "CERTIFICATE" { cert, err := x509.ParseCertificate(certDERBlock.Bytes) if err != nil { return nil, err } certificates = append(certificates, cert) } } if len(certificates) == 0 { return nil, errors.New("No certificates were found while parsing the bundle.") } return certificates, nil } func parsePEMPrivateKey(key []byte) (crypto.PrivateKey, error) { keyBlock, _ := pem.Decode(key) switch keyBlock.Type { case "RSA PRIVATE KEY": return x509.ParsePKCS1PrivateKey(keyBlock.Bytes) case "EC PRIVATE KEY": return x509.ParseECPrivateKey(keyBlock.Bytes) default: return nil, errors.New("Unknown PEM header value") } } func generatePrivateKey(keyType KeyType) (crypto.PrivateKey, error) { switch keyType { case EC256: return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) case EC384: return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) case RSA2048: return rsa.GenerateKey(rand.Reader, 2048) case RSA4096: return rsa.GenerateKey(rand.Reader, 4096) case RSA8192: return rsa.GenerateKey(rand.Reader, 8192) } return nil, fmt.Errorf("Invalid KeyType: %s", keyType) } func generateCsr(privateKey crypto.PrivateKey, domain string, san []string) ([]byte, error) { template := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, }, } if len(san) > 0 { template.DNSNames = san } return x509.CreateCertificateRequest(rand.Reader, &template, privateKey) } func pemEncode(data interface{}) []byte { var pemBlock *pem.Block switch key := data.(type) { case *ecdsa.PrivateKey: keyBytes, _ := x509.MarshalECPrivateKey(key) pemBlock = &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes} case *rsa.PrivateKey: pemBlock = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)} break case derCertificateBytes: pemBlock = &pem.Block{Type: "CERTIFICATE", Bytes: []byte(data.(derCertificateBytes))} } return pem.EncodeToMemory(pemBlock) } func pemDecode(data []byte) (*pem.Block, error) { pemBlock, _ := pem.Decode(data) if pemBlock == nil { return nil, fmt.Errorf("Pem decode did not yield a valid block. Is the certificate in the right format?") } return pemBlock, nil } func pemDecodeTox509(pem []byte) (*x509.Certificate, error) { pemBlock, err := pemDecode(pem) if pemBlock == nil { return nil, err } return x509.ParseCertificate(pemBlock.Bytes) } // GetPEMCertExpiration returns the "NotAfter" date of a PEM encoded certificate. // The certificate has to be PEM encoded. Any other encodings like DER will fail. func GetPEMCertExpiration(cert []byte) (time.Time, error) { pemBlock, err := pemDecode(cert) if pemBlock == nil { return time.Time{}, err } return getCertExpiration(pemBlock.Bytes) } // getCertExpiration returns the "NotAfter" date of a DER encoded certificate. func getCertExpiration(cert []byte) (time.Time, error) { pCert, err := x509.ParseCertificate(cert) if err != nil { return time.Time{}, err } return pCert.NotAfter, nil } func generatePemCert(privKey *rsa.PrivateKey, domain string) ([]byte, error) { derBytes, err := generateDerCert(privKey, time.Time{}, domain) if err != nil { return nil, err } return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}), nil } func generateDerCert(privKey *rsa.PrivateKey, expiration time.Time, domain string) ([]byte, error) { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return nil, err } if expiration.IsZero() { expiration = time.Now().Add(365) } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: "ACME Challenge TEMP", }, NotBefore: time.Now(), NotAfter: expiration, KeyUsage: x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, DNSNames: []string{domain}, } return x509.CreateCertificate(rand.Reader, &template, &template, &privKey.PublicKey, privKey) } func limitReader(rd io.ReadCloser, numBytes int64) io.ReadCloser { return http.MaxBytesReader(nil, rd, numBytes) } crypto_test.go000066400000000000000000000045751313450123100340270ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "bytes" "crypto/rand" "crypto/rsa" "testing" "time" ) func TestGeneratePrivateKey(t *testing.T) { key, err := generatePrivateKey(RSA2048) if err != nil { t.Error("Error generating private key:", err) } if key == nil { t.Error("Expected key to not be nil, but it was") } } func TestGenerateCSR(t *testing.T) { key, err := rsa.GenerateKey(rand.Reader, 512) if err != nil { t.Fatal("Error generating private key:", err) } csr, err := generateCsr(key, "fizz.buzz", nil) if err != nil { t.Error("Error generating CSR:", err) } if csr == nil || len(csr) == 0 { t.Error("Expected CSR with data, but it was nil or length 0") } } func TestPEMEncode(t *testing.T) { buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") reader := MockRandReader{b: buf} key, err := rsa.GenerateKey(reader, 32) if err != nil { t.Fatal("Error generating private key:", err) } data := pemEncode(key) if data == nil { t.Fatal("Expected result to not be nil, but it was") } if len(data) != 127 { t.Errorf("Expected PEM encoding to be length 127, but it was %d", len(data)) } } func TestPEMCertExpiration(t *testing.T) { privKey, err := generatePrivateKey(RSA2048) if err != nil { t.Fatal("Error generating private key:", err) } expiration := time.Now().Add(365) expiration = expiration.Round(time.Second) certBytes, err := generateDerCert(privKey.(*rsa.PrivateKey), expiration, "test.com") if err != nil { t.Fatal("Error generating cert:", err) } buf := bytes.NewBufferString("TestingRSAIsSoMuchFun") // Some random string should return an error. if ctime, err := GetPEMCertExpiration(buf.Bytes()); err == nil { t.Errorf("Expected getCertExpiration to return an error for garbage string but returned %v", ctime) } // A DER encoded certificate should return an error. if _, err := GetPEMCertExpiration(certBytes); err == nil { t.Errorf("Expected getCertExpiration to return an error for DER certificates but returned none.") } // A PEM encoded certificate should work ok. pemCert := pemEncode(derCertificateBytes(certBytes)) if ctime, err := GetPEMCertExpiration(pemCert); err != nil || !ctime.Equal(expiration.UTC()) { t.Errorf("Expected getCertExpiration to return %v but returned %v. Error: %v", expiration, ctime, err) } } type MockRandReader struct { b *bytes.Buffer } func (r MockRandReader) Read(p []byte) (int, error) { return r.b.Read(p) } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/error.go000066400000000000000000000033621313450123100326510ustar00rootroot00000000000000package acme import ( "encoding/json" "fmt" "net/http" "strings" ) const ( tosAgreementError = "Must agree to subscriber agreement before any further actions" ) // RemoteError is the base type for all errors specific to the ACME protocol. type RemoteError struct { StatusCode int `json:"status,omitempty"` Type string `json:"type"` Detail string `json:"detail"` } func (e RemoteError) Error() string { return fmt.Sprintf("acme: Error %d - %s - %s", e.StatusCode, e.Type, e.Detail) } // TOSError represents the error which is returned if the user needs to // accept the TOS. // TODO: include the new TOS url if we can somehow obtain it. type TOSError struct { RemoteError } type domainError struct { Domain string Error error } type challengeError struct { RemoteError records []validationRecord } func (c challengeError) Error() string { var errStr string for _, validation := range c.records { errStr = errStr + fmt.Sprintf("\tValidation for %s:%s\n\tResolved to:\n\t\t%s\n\tUsed: %s\n\n", validation.Hostname, validation.Port, strings.Join(validation.ResolvedAddresses, "\n\t\t"), validation.UsedAddress) } return fmt.Sprintf("%s\nError Detail:\n%s", c.RemoteError.Error(), errStr) } func handleHTTPError(resp *http.Response) error { var errorDetail RemoteError decoder := json.NewDecoder(resp.Body) err := decoder.Decode(&errorDetail) if err != nil { return err } errorDetail.StatusCode = resp.StatusCode // Check for errors we handle specifically if errorDetail.StatusCode == http.StatusForbidden && errorDetail.Detail == tosAgreementError { return TOSError{errorDetail} } return errorDetail } func handleChallengeError(chlng challenge) error { return challengeError{chlng.Error, chlng.ValidationRecords} } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/http.go000066400000000000000000000063401313450123100324760ustar00rootroot00000000000000package acme import ( "encoding/json" "errors" "fmt" "io" "net/http" "runtime" "strings" "time" ) // UserAgent (if non-empty) will be tacked onto the User-Agent string in requests. var UserAgent string // defaultClient is an HTTP client with a reasonable timeout value. var defaultClient = http.Client{Timeout: 10 * time.Second} const ( // defaultGoUserAgent is the Go HTTP package user agent string. Too // bad it isn't exported. If it changes, we should update it here, too. defaultGoUserAgent = "Go-http-client/1.1" // ourUserAgent is the User-Agent of this underlying library package. ourUserAgent = "xenolf-acme" ) // httpHead performs a HEAD request with a proper User-Agent string. // The response body (resp.Body) is already closed when this function returns. func httpHead(url string) (resp *http.Response, err error) { req, err := http.NewRequest("HEAD", url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", userAgent()) resp, err = defaultClient.Do(req) if err != nil { return resp, err } resp.Body.Close() return resp, err } // httpPost performs a POST request with a proper User-Agent string. // Callers should close resp.Body when done reading from it. func httpPost(url string, bodyType string, body io.Reader) (resp *http.Response, err error) { req, err := http.NewRequest("POST", url, body) if err != nil { return nil, err } req.Header.Set("Content-Type", bodyType) req.Header.Set("User-Agent", userAgent()) return defaultClient.Do(req) } // httpGet performs a GET request with a proper User-Agent string. // Callers should close resp.Body when done reading from it. func httpGet(url string) (resp *http.Response, err error) { req, err := http.NewRequest("GET", url, nil) if err != nil { return nil, err } req.Header.Set("User-Agent", userAgent()) return defaultClient.Do(req) } // getJSON performs an HTTP GET request and parses the response body // as JSON, into the provided respBody object. func getJSON(uri string, respBody interface{}) (http.Header, error) { resp, err := httpGet(uri) if err != nil { return nil, fmt.Errorf("failed to get %q: %v", uri, err) } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return resp.Header, handleHTTPError(resp) } return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) } // postJSON performs an HTTP POST request and parses the response body // as JSON, into the provided respBody object. func postJSON(j *jws, uri string, reqBody, respBody interface{}) (http.Header, error) { jsonBytes, err := json.Marshal(reqBody) if err != nil { return nil, errors.New("Failed to marshal network message...") } resp, err := j.post(uri, jsonBytes) if err != nil { return nil, fmt.Errorf("Failed to post JWS message. -> %v", err) } defer resp.Body.Close() if resp.StatusCode >= http.StatusBadRequest { return resp.Header, handleHTTPError(resp) } if respBody == nil { return resp.Header, nil } return resp.Header, json.NewDecoder(resp.Body).Decode(respBody) } // userAgent builds and returns the User-Agent string to use in requests. func userAgent() string { ua := fmt.Sprintf("%s (%s; %s) %s %s", defaultGoUserAgent, runtime.GOOS, runtime.GOARCH, ourUserAgent, UserAgent) return strings.TrimSpace(ua) } http_challenge.go000066400000000000000000000020161313450123100344150ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "fmt" "log" ) type httpChallenge struct { jws *jws validate validateFunc provider ChallengeProvider } // HTTP01ChallengePath returns the URL path for the `http-01` challenge func HTTP01ChallengePath(token string) string { return "/.well-known/acme-challenge/" + token } func (s *httpChallenge) Solve(chlng challenge, domain string) error { logf("[INFO][%s] acme: Trying to solve HTTP-01", domain) // Generate the Key Authorization for the challenge keyAuth, err := getKeyAuthorization(chlng.Token, s.jws.privKey) if err != nil { return err } err = s.provider.Present(domain, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] error presenting token: %v", domain, err) } defer func() { err := s.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { log.Printf("[%s] error cleaning up: %v", domain, err) } }() return s.validate(s.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } http_challenge_server.go000066400000000000000000000045001313450123100360030ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "fmt" "net" "net/http" "strings" ) // HTTPProviderServer implements ChallengeProvider for `http-01` challenge // It may be instantiated without using the NewHTTPProviderServer function if // you want only to use the default values. type HTTPProviderServer struct { iface string port string done chan bool listener net.Listener } // NewHTTPProviderServer creates a new HTTPProviderServer on the selected interface and port. // Setting iface and / or port to an empty string will make the server fall back to // the "any" interface and port 80 respectively. func NewHTTPProviderServer(iface, port string) *HTTPProviderServer { return &HTTPProviderServer{iface: iface, port: port} } // Present starts a web server and makes the token available at `HTTP01ChallengePath(token)` for web requests. func (s *HTTPProviderServer) Present(domain, token, keyAuth string) error { if s.port == "" { s.port = "80" } var err error s.listener, err = net.Listen("tcp", net.JoinHostPort(s.iface, s.port)) if err != nil { return fmt.Errorf("Could not start HTTP server for challenge -> %v", err) } s.done = make(chan bool) go s.serve(domain, token, keyAuth) return nil } // CleanUp closes the HTTP server and removes the token from `HTTP01ChallengePath(token)` func (s *HTTPProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } s.listener.Close() <-s.done return nil } func (s *HTTPProviderServer) serve(domain, token, keyAuth string) { path := HTTP01ChallengePath(token) // The handler validates the HOST header and request type. // For validation it then writes the token the server returned with the challenge mux := http.NewServeMux() mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.Host, domain) && r.Method == "GET" { w.Header().Add("Content-Type", "text/plain") w.Write([]byte(keyAuth)) logf("[INFO][%s] Served key authentication", domain) } else { logf("[INFO] Received request for domain %s with method %s", r.Host, r.Method) w.Write([]byte("TEST")) } }) httpServer := &http.Server{ Handler: mux, } // Once httpServer is shut down we don't want any lingering // connections, so disable KeepAlives. httpServer.SetKeepAlivesEnabled(false) httpServer.Serve(s.listener) s.done <- true } http_challenge_test.go000066400000000000000000000033041313450123100354550ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "crypto/rand" "crypto/rsa" "io/ioutil" "strings" "testing" ) func TestHTTPChallenge(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 512) j := &jws{privKey: privKey} clientChallenge := challenge{Type: HTTP01, Token: "http1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { uri := "http://localhost:23457/.well-known/acme-challenge/" + chlng.Token resp, err := httpGet(uri) if err != nil { return err } defer resp.Body.Close() if want := "text/plain"; resp.Header.Get("Content-Type") != want { t.Errorf("Get(%q) Content-Type: got %q, want %q", uri, resp.Header.Get("Content-Type"), want) } body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } bodyStr := string(body) if bodyStr != chlng.KeyAuthorization { t.Errorf("Get(%q) Body: got %q, want %q", uri, bodyStr, chlng.KeyAuthorization) } return nil } solver := &httpChallenge{jws: j, validate: mockValidate, provider: &HTTPProviderServer{port: "23457"}} if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { t.Errorf("Solve error: got %v, want nil", err) } } func TestHTTPChallengeInvalidPort(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 128) j := &jws{privKey: privKey} clientChallenge := challenge{Type: HTTP01, Token: "http2"} solver := &httpChallenge{jws: j, validate: stubValidate, provider: &HTTPProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { t.Errorf("Solve error: got %v, want error", err) } else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) { t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/http_test.go000066400000000000000000000047321313450123100335400ustar00rootroot00000000000000package acme import ( "net/http" "net/http/httptest" "strings" "testing" ) func TestHTTPHeadUserAgent(t *testing.T) { var ua, method string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") method = r.Method })) defer ts.Close() _, err := httpHead(ts.URL) if err != nil { t.Fatal(err) } if method != "HEAD" { t.Errorf("Expected method to be HEAD, got %s", method) } if !strings.Contains(ua, ourUserAgent) { t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) } } func TestHTTPGetUserAgent(t *testing.T) { var ua, method string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") method = r.Method })) defer ts.Close() res, err := httpGet(ts.URL) if err != nil { t.Fatal(err) } res.Body.Close() if method != "GET" { t.Errorf("Expected method to be GET, got %s", method) } if !strings.Contains(ua, ourUserAgent) { t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) } } func TestHTTPPostUserAgent(t *testing.T) { var ua, method string ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ua = r.Header.Get("User-Agent") method = r.Method })) defer ts.Close() res, err := httpPost(ts.URL, "text/plain", strings.NewReader("falalalala")) if err != nil { t.Fatal(err) } res.Body.Close() if method != "POST" { t.Errorf("Expected method to be POST, got %s", method) } if !strings.Contains(ua, ourUserAgent) { t.Errorf("Expected User-Agent to contain '%s', got: '%s'", ourUserAgent, ua) } } func TestUserAgent(t *testing.T) { ua := userAgent() if !strings.Contains(ua, defaultGoUserAgent) { t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua) } if !strings.Contains(ua, ourUserAgent) { t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua) } if strings.HasSuffix(ua, " ") { t.Errorf("UA should not have trailing spaces; got '%s'", ua) } // customize the UA by appending a value UserAgent = "MyApp/1.2.3" ua = userAgent() if !strings.Contains(ua, defaultGoUserAgent) { t.Errorf("Expected UA to contain %s, got '%s'", defaultGoUserAgent, ua) } if !strings.Contains(ua, ourUserAgent) { t.Errorf("Expected UA to contain %s, got '%s'", ourUserAgent, ua) } if !strings.Contains(ua, UserAgent) { t.Errorf("Expected custom UA to contain %s, got '%s'", UserAgent, ua) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/jws.go000066400000000000000000000040471313450123100323240ustar00rootroot00000000000000package acme import ( "bytes" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rsa" "fmt" "net/http" "gopkg.in/square/go-jose.v1" ) type jws struct { directoryURL string privKey crypto.PrivateKey nonces []string } func keyAsJWK(key interface{}) *jose.JsonWebKey { switch k := key.(type) { case *ecdsa.PublicKey: return &jose.JsonWebKey{Key: k, Algorithm: "EC"} case *rsa.PublicKey: return &jose.JsonWebKey{Key: k, Algorithm: "RSA"} default: return nil } } // Posts a JWS signed message to the specified URL func (j *jws) post(url string, content []byte) (*http.Response, error) { signedContent, err := j.signContent(content) if err != nil { return nil, err } resp, err := httpPost(url, "application/jose+json", bytes.NewBuffer([]byte(signedContent.FullSerialize()))) if err != nil { return nil, err } j.getNonceFromResponse(resp) return resp, err } func (j *jws) signContent(content []byte) (*jose.JsonWebSignature, error) { var alg jose.SignatureAlgorithm switch k := j.privKey.(type) { case *rsa.PrivateKey: alg = jose.RS256 case *ecdsa.PrivateKey: if k.Curve == elliptic.P256() { alg = jose.ES256 } else if k.Curve == elliptic.P384() { alg = jose.ES384 } } signer, err := jose.NewSigner(alg, j.privKey) if err != nil { return nil, err } signer.SetNonceSource(j) signed, err := signer.Sign(content) if err != nil { return nil, err } return signed, nil } func (j *jws) getNonceFromResponse(resp *http.Response) error { nonce := resp.Header.Get("Replay-Nonce") if nonce == "" { return fmt.Errorf("Server did not respond with a proper nonce header.") } j.nonces = append(j.nonces, nonce) return nil } func (j *jws) getNonce() error { resp, err := httpHead(j.directoryURL) if err != nil { return err } return j.getNonceFromResponse(resp) } func (j *jws) Nonce() (string, error) { nonce := "" if len(j.nonces) == 0 { err := j.getNonce() if err != nil { return nonce, err } } nonce, j.nonces = j.nonces[len(j.nonces)-1], j.nonces[:len(j.nonces)-1] return nonce, nil } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/messages.go000066400000000000000000000076351313450123100333360ustar00rootroot00000000000000package acme import ( "time" "gopkg.in/square/go-jose.v1" ) type directory struct { NewAuthzURL string `json:"new-authz"` NewCertURL string `json:"new-cert"` NewRegURL string `json:"new-reg"` RevokeCertURL string `json:"revoke-cert"` } type recoveryKeyMessage struct { Length int `json:"length,omitempty"` Client jose.JsonWebKey `json:"client,omitempty"` Server jose.JsonWebKey `json:"client,omitempty"` } type registrationMessage struct { Resource string `json:"resource"` Contact []string `json:"contact"` // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"` } // Registration is returned by the ACME server after the registration // The client implementation should save this registration somewhere. type Registration struct { Resource string `json:"resource,omitempty"` ID int `json:"id"` Key jose.JsonWebKey `json:"key"` Contact []string `json:"contact"` Agreement string `json:"agreement,omitempty"` Authorizations string `json:"authorizations,omitempty"` Certificates string `json:"certificates,omitempty"` // RecoveryKey recoveryKeyMessage `json:"recoveryKey,omitempty"` } // RegistrationResource represents all important informations about a registration // of which the client needs to keep track itself. type RegistrationResource struct { Body Registration `json:"body,omitempty"` URI string `json:"uri,omitempty"` NewAuthzURL string `json:"new_authzr_uri,omitempty"` TosURL string `json:"terms_of_service,omitempty"` } type authorizationResource struct { Body authorization Domain string NewCertURL string AuthURL string } type authorization struct { Resource string `json:"resource,omitempty"` Identifier identifier `json:"identifier"` Status string `json:"status,omitempty"` Expires time.Time `json:"expires,omitempty"` Challenges []challenge `json:"challenges,omitempty"` Combinations [][]int `json:"combinations,omitempty"` } type identifier struct { Type string `json:"type"` Value string `json:"value"` } type validationRecord struct { URI string `json:"url,omitempty"` Hostname string `json:"hostname,omitempty"` Port string `json:"port,omitempty"` ResolvedAddresses []string `json:"addressesResolved,omitempty"` UsedAddress string `json:"addressUsed,omitempty"` } type challenge struct { Resource string `json:"resource,omitempty"` Type Challenge `json:"type,omitempty"` Status string `json:"status,omitempty"` URI string `json:"uri,omitempty"` Token string `json:"token,omitempty"` KeyAuthorization string `json:"keyAuthorization,omitempty"` TLS bool `json:"tls,omitempty"` Iterations int `json:"n,omitempty"` Error RemoteError `json:"error,omitempty"` ValidationRecords []validationRecord `json:"validationRecord,omitempty"` } type csrMessage struct { Resource string `json:"resource,omitempty"` Csr string `json:"csr"` Authorizations []string `json:"authorizations"` } type revokeCertMessage struct { Resource string `json:"resource"` Certificate string `json:"certificate"` } // CertificateResource represents a CA issued certificate. // PrivateKey and Certificate are both already PEM encoded // and can be directly written to disk. Certificate may // be a certificate bundle, depending on the options supplied // to create it. type CertificateResource struct { Domain string `json:"domain"` CertURL string `json:"certUrl"` CertStableURL string `json:"certStableUrl"` AccountRef string `json:"accountRef,omitempty"` PrivateKey []byte `json:"-"` Certificate []byte `json:"-"` } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/provider.go000066400000000000000000000021411313450123100333440ustar00rootroot00000000000000package acme import "time" // ChallengeProvider enables implementing a custom challenge // provider. Present presents the solution to a challenge available to // be solved. CleanUp will be called by the challenge if Present ends // in a non-error state. type ChallengeProvider interface { Present(domain, token, keyAuth string) error CleanUp(domain, token, keyAuth string) error } // ChallengeProviderTimeout allows for implementing a // ChallengeProvider where an unusually long timeout is required when // waiting for an ACME challenge to be satisfied, such as when // checking for DNS record progagation. If an implementor of a // ChallengeProvider provides a Timeout method, then the return values // of the Timeout method will be used when appropriate by the acme // package. The interval value is the time between checks. // // The default values used for timeout and interval are 60 seconds and // 2 seconds respectively. These are used when no Timeout method is // defined for the ChallengeProvider. type ChallengeProviderTimeout interface { ChallengeProvider Timeout() (timeout, interval time.Duration) } tls_sni_challenge.go000066400000000000000000000041231313450123100351120ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "crypto/rsa" "crypto/sha256" "crypto/tls" "encoding/hex" "fmt" "log" ) type tlsSNIChallenge struct { jws *jws validate validateFunc provider ChallengeProvider } func (t *tlsSNIChallenge) Solve(chlng challenge, domain string) error { // FIXME: https://github.com/ietf-wg-acme/acme/pull/22 // Currently we implement this challenge to track boulder, not the current spec! logf("[INFO][%s] acme: Trying to solve TLS-SNI-01", domain) // Generate the Key Authorization for the challenge keyAuth, err := getKeyAuthorization(chlng.Token, t.jws.privKey) if err != nil { return err } err = t.provider.Present(domain, chlng.Token, keyAuth) if err != nil { return fmt.Errorf("[%s] error presenting token: %v", domain, err) } defer func() { err := t.provider.CleanUp(domain, chlng.Token, keyAuth) if err != nil { log.Printf("[%s] error cleaning up: %v", domain, err) } }() return t.validate(t.jws, domain, chlng.URI, challenge{Resource: "challenge", Type: chlng.Type, Token: chlng.Token, KeyAuthorization: keyAuth}) } // TLSSNI01ChallengeCert returns a certificate and target domain for the `tls-sni-01` challenge func TLSSNI01ChallengeCertDomain(keyAuth string) (tls.Certificate, string, error) { // generate a new RSA key for the certificates tempPrivKey, err := generatePrivateKey(RSA2048) if err != nil { return tls.Certificate{}, "", err } rsaPrivKey := tempPrivKey.(*rsa.PrivateKey) rsaPrivPEM := pemEncode(rsaPrivKey) zBytes := sha256.Sum256([]byte(keyAuth)) z := hex.EncodeToString(zBytes[:sha256.Size]) domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) tempCertPEM, err := generatePemCert(rsaPrivKey, domain) if err != nil { return tls.Certificate{}, "", err } certificate, err := tls.X509KeyPair(tempCertPEM, rsaPrivPEM) if err != nil { return tls.Certificate{}, "", err } return certificate, domain, nil } // TLSSNI01ChallengeCert returns a certificate for the `tls-sni-01` challenge func TLSSNI01ChallengeCert(keyAuth string) (tls.Certificate, error) { cert, _, err := TLSSNI01ChallengeCertDomain(keyAuth) return cert, err } tls_sni_challenge_server.go000066400000000000000000000027641313450123100365110ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "crypto/tls" "fmt" "net" "net/http" ) // TLSProviderServer implements ChallengeProvider for `TLS-SNI-01` challenge // It may be instantiated without using the NewTLSProviderServer function if // you want only to use the default values. type TLSProviderServer struct { iface string port string done chan bool listener net.Listener } // NewTLSProviderServer creates a new TLSProviderServer on the selected interface and port. // Setting iface and / or port to an empty string will make the server fall back to // the "any" interface and port 443 respectively. func NewTLSProviderServer(iface, port string) *TLSProviderServer { return &TLSProviderServer{iface: iface, port: port} } // Present makes the keyAuth available as a cert func (s *TLSProviderServer) Present(domain, token, keyAuth string) error { if s.port == "" { s.port = "443" } cert, err := TLSSNI01ChallengeCert(keyAuth) if err != nil { return err } tlsConf := new(tls.Config) tlsConf.Certificates = []tls.Certificate{cert} s.listener, err = tls.Listen("tcp", net.JoinHostPort(s.iface, s.port), tlsConf) if err != nil { return fmt.Errorf("Could not start HTTPS server for challenge -> %v", err) } s.done = make(chan bool) go func() { http.Serve(s.listener, nil) s.done <- true }() return nil } // CleanUp closes the HTTP server. func (s *TLSProviderServer) CleanUp(domain, token, keyAuth string) error { if s.listener == nil { return nil } s.listener.Close() <-s.done return nil } tls_sni_challenge_test.go000066400000000000000000000042451313450123100361560ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acmepackage acme import ( "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" "encoding/hex" "fmt" "strings" "testing" ) func TestTLSSNIChallenge(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 512) j := &jws{privKey: privKey} clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni1"} mockValidate := func(_ *jws, _, _ string, chlng challenge) error { conn, err := tls.Dial("tcp", "localhost:23457", &tls.Config{ InsecureSkipVerify: true, }) if err != nil { t.Errorf("Expected to connect to challenge server without an error. %s", err.Error()) } // Expect the server to only return one certificate connState := conn.ConnectionState() if count := len(connState.PeerCertificates); count != 1 { t.Errorf("Expected the challenge server to return exactly one certificate but got %d", count) } remoteCert := connState.PeerCertificates[0] if count := len(remoteCert.DNSNames); count != 1 { t.Errorf("Expected the challenge certificate to have exactly one DNSNames entry but had %d", count) } zBytes := sha256.Sum256([]byte(chlng.KeyAuthorization)) z := hex.EncodeToString(zBytes[:sha256.Size]) domain := fmt.Sprintf("%s.%s.acme.invalid", z[:32], z[32:]) if remoteCert.DNSNames[0] != domain { t.Errorf("Expected the challenge certificate DNSName to match %s but was %s", domain, remoteCert.DNSNames[0]) } return nil } solver := &tlsSNIChallenge{jws: j, validate: mockValidate, provider: &TLSProviderServer{port: "23457"}} if err := solver.Solve(clientChallenge, "localhost:23457"); err != nil { t.Errorf("Solve error: got %v, want nil", err) } } func TestTLSSNIChallengeInvalidPort(t *testing.T) { privKey, _ := rsa.GenerateKey(rand.Reader, 128) j := &jws{privKey: privKey} clientChallenge := challenge{Type: TLSSNI01, Token: "tlssni2"} solver := &tlsSNIChallenge{jws: j, validate: stubValidate, provider: &TLSProviderServer{port: "123456"}} if err := solver.Solve(clientChallenge, "localhost:123456"); err == nil { t.Errorf("Solve error: got %v, want error", err) } else if want := "invalid port 123456"; !strings.HasSuffix(err.Error(), want) { t.Errorf("Solve error: got %q, want suffix %q", err.Error(), want) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/utils.go000066400000000000000000000007601313450123100326570ustar00rootroot00000000000000package acme import ( "fmt" "time" ) // WaitFor polls the given function 'f', once every 'interval', up to 'timeout'. func WaitFor(timeout, interval time.Duration, f func() (bool, error)) error { var lastErr string timeup := time.After(timeout) for { select { case <-timeup: return fmt.Errorf("Time limit exceeded. Last error: %s", lastErr) default: } stop, err := f() if stop { return nil } if err != nil { lastErr = err.Error() } time.Sleep(interval) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/github.com/xenolf/lego/acme/utils_test.go000066400000000000000000000006531313450123100337170ustar00rootroot00000000000000package acme import ( "testing" "time" ) func TestWaitForTimeout(t *testing.T) { c := make(chan error) go func() { err := WaitFor(3*time.Second, 1*time.Second, func() (bool, error) { return false, nil }) c <- err }() timeout := time.After(4 * time.Second) select { case <-timeout: t.Fatal("timeout exceeded") case err := <-c: if err == nil { t.Errorf("expected timeout error; got %v", err) } } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/000077500000000000000000000000001313450123100255145ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/000077500000000000000000000000001313450123100270145ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/000077500000000000000000000000001313450123100307045ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/BUG-BOUNTY.md000066400000000000000000000005571313450123100326700ustar00rootroot00000000000000Serious about security ====================== Square recognizes the important contributions the security research community can make. We therefore encourage reporting security issues with the code contained in this repository. If you believe you have discovered a security vulnerability, please follow the guidelines at . CONTRIBUTING.md000066400000000000000000000012271313450123100330600ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1# Contributing If you would like to contribute code to go-jose you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure all tests pass by running `go test`, and format your code with `go fmt`. We also recommend using `golint` and `errcheck`. Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement][1]. [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/LICENSE000066400000000000000000000261361313450123100317210ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/README.md000066400000000000000000000214251313450123100321670ustar00rootroot00000000000000# Go JOSE [![godoc](http://img.shields.io/badge/godoc-reference-blue.svg?style=flat)](https://godoc.org/gopkg.in/square/go-jose.v1) [![license](http://img.shields.io/badge/license-apache_2.0-red.svg?style=flat)](https://raw.githubusercontent.com/square/go-jose/master/LICENSE) [![build](https://travis-ci.org/square/go-jose.svg?branch=master)](https://travis-ci.org/square/go-jose) [![coverage](https://coveralls.io/repos/github/square/go-jose/badge.svg?branch=master)](https://coveralls.io/r/square/go-jose) Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. For the moment, it mainly focuses on encryption and signing based on the JSON Web Encryption and JSON Web Signature standards. **Disclaimer**: This library contains encryption software that is subject to the U.S. Export Administration Regulations. You may not export, re-export, transfer or download this code or any part of it in violation of any United States law, directive or regulation. In particular this software may not be exported or re-exported in any form or on any media to Iran, North Sudan, Syria, Cuba, or North Korea, or to denied persons or entities mentioned on any US maintained blocked list. ## Overview The implementation follows the [JSON Web Encryption](http://dx.doi.org/10.17487/RFC7516) standard (RFC 7516) and [JSON Web Signature](http://dx.doi.org/10.17487/RFC7515) standard (RFC 7515). Tables of supported algorithms are shown below. The library supports both the compact and full serialization formats, and has optional support for multiple recipients. It also comes with a small command-line utility ([`jose-util`](https://github.com/square/go-jose/tree/master/jose-util)) for dealing with JOSE messages in a shell. **Note**: We use a forked version of the `encoding/json` package from the Go standard library which uses case-sensitive matching for member names (instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html)). This is to avoid differences in interpretation of messages between go-jose and libraries in other languages. If you do not like this behavior, you can use the `std_json` build tag to disable it (though we do not recommend doing so). ### Versions We use [gopkg.in](https://gopkg.in) for versioning. [Version 1](https://gopkg.in/square/go-jose.v1) is the current stable version: import "gopkg.in/square/go-jose.v1" The interface for [go-jose.v1](https://gopkg.in/square/go-jose.v1) will remain backwards compatible. We're currently sketching out ideas for a new version, to clean up the interface a bit. If you have ideas or feature requests [please let us know](https://github.com/square/go-jose/issues/64)! ### Supported algorithms See below for a table of supported algorithms. Algorithm identifiers match the names in the [JSON Web Algorithms](http://dx.doi.org/10.17487/RFC7518) standard where possible. The [Godoc reference](https://godoc.org/github.com/square/go-jose#pkg-constants) has a list of constants. Key encryption | Algorithm identifier(s) :------------------------- | :------------------------------ RSA-PKCS#1v1.5 | RSA1_5 RSA-OAEP | RSA-OAEP, RSA-OAEP-256 AES key wrap | A128KW, A192KW, A256KW AES-GCM key wrap | A128GCMKW, A192GCMKW, A256GCMKW ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW ECDH-ES (direct) | ECDH-ES1 Direct encryption | dir1 1. Not supported in multi-recipient mode Signing / MAC | Algorithm identifier(s) :------------------------- | :------------------------------ RSASSA-PKCS#1v1.5 | RS256, RS384, RS512 RSASSA-PSS | PS256, PS384, PS512 HMAC | HS256, HS384, HS512 ECDSA | ES256, ES384, ES512 Content encryption | Algorithm identifier(s) :------------------------- | :------------------------------ AES-CBC+HMAC | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 AES-GCM | A128GCM, A192GCM, A256GCM Compression | Algorithm identifiers(s) :------------------------- | ------------------------------- DEFLATE (RFC 1951) | DEF ### Supported key types See below for a table of supported key types. These are understood by the library, and can be passed to corresponding functions such as `NewEncrypter` or `NewSigner`. Note that if you are creating a new encrypter or signer with a JsonWebKey, the key id of the JsonWebKey (if present) will be added to any resulting messages. Algorithm(s) | Corresponding types :------------------------- | ------------------------------- RSA | *[rsa.PublicKey](http://golang.org/pkg/crypto/rsa/#PublicKey), *[rsa.PrivateKey](http://golang.org/pkg/crypto/rsa/#PrivateKey), *[jose.JsonWebKey](https://godoc.org/github.com/square/go-jose#JsonWebKey) ECDH, ECDSA | *[ecdsa.PublicKey](http://golang.org/pkg/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](http://golang.org/pkg/crypto/ecdsa/#PrivateKey), *[jose.JsonWebKey](https://godoc.org/github.com/square/go-jose#JsonWebKey) AES, HMAC | []byte, *[jose.JsonWebKey](https://godoc.org/github.com/square/go-jose#JsonWebKey) ## Examples Encryption/decryption example using RSA: ```Go // Generate a public/private key pair to use for this example. The library // also provides two utility functions (LoadPublicKey and LoadPrivateKey) // that can be used to load keys from PEM/DER-encoded data. privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } // Instantiate an encrypter using RSA-OAEP with AES128-GCM. An error would // indicate that the selected algorithm(s) are not currently supported. publicKey := &privateKey.PublicKey encrypter, err := NewEncrypter(RSA_OAEP, A128GCM, publicKey) if err != nil { panic(err) } // Encrypt a sample plaintext. Calling the encrypter returns an encrypted // JWE object, which can then be serialized for output afterwards. An error // would indicate a problem in an underlying cryptographic primitive. var plaintext = []byte("Lorem ipsum dolor sit amet") object, err := encrypter.Encrypt(plaintext) if err != nil { panic(err) } // Serialize the encrypted object using the full serialization format. // Alternatively you can also use the compact format here by calling // object.CompactSerialize() instead. serialized := object.FullSerialize() // Parse the serialized, encrypted JWE object. An error would indicate that // the given input did not represent a valid message. object, err = ParseEncrypted(serialized) if err != nil { panic(err) } // Now we can decrypt and get back our original plaintext. An error here // would indicate the the message failed to decrypt, e.g. because the auth // tag was broken or the message was tampered with. decrypted, err := object.Decrypt(privateKey) if err != nil { panic(err) } fmt.Printf(string(decrypted)) // output: Lorem ipsum dolor sit amet ``` Signing/verification example using RSA: ```Go // Generate a public/private key pair to use for this example. The library // also provides two utility functions (LoadPublicKey and LoadPrivateKey) // that can be used to load keys from PEM/DER-encoded data. privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } // Instantiate a signer using RSASSA-PSS (SHA512) with the given private key. signer, err := NewSigner(PS512, privateKey) if err != nil { panic(err) } // Sign a sample payload. Calling the signer returns a protected JWS object, // which can then be serialized for output afterwards. An error would // indicate a problem in an underlying cryptographic primitive. var payload = []byte("Lorem ipsum dolor sit amet") object, err := signer.Sign(payload) if err != nil { panic(err) } // Serialize the encrypted object using the full serialization format. // Alternatively you can also use the compact format here by calling // object.CompactSerialize() instead. serialized := object.FullSerialize() // Parse the serialized, protected JWS object. An error would indicate that // the given input did not represent a valid message. object, err = ParseSigned(serialized) if err != nil { panic(err) } // Now we can verify the signature on the payload. An error here would // indicate the the message failed to verify, e.g. because the signature was // broken or the message was tampered with. output, err := object.Verify(&privateKey.PublicKey) if err != nil { panic(err) } fmt.Printf(string(output)) // output: Lorem ipsum dolor sit amet ``` More examples can be found in the [Godoc reference](https://godoc.org/github.com/square/go-jose) for this package. The [`jose-util`](https://github.com/square/go-jose/tree/master/jose-util) subdirectory also contains a small command-line utility which might be useful as an example. docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/asymmetric.go000066400000000000000000000322051313450123100334120ustar00rootroot00000000000000/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto" "crypto/aes" "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/sha256" "errors" "fmt" "math/big" "gopkg.in/square/go-jose.v1/cipher" ) // A generic RSA-based encrypter/verifier type rsaEncrypterVerifier struct { publicKey *rsa.PublicKey } // A generic RSA-based decrypter/signer type rsaDecrypterSigner struct { privateKey *rsa.PrivateKey } // A generic EC-based encrypter/verifier type ecEncrypterVerifier struct { publicKey *ecdsa.PublicKey } // A key generator for ECDH-ES type ecKeyGenerator struct { size int algID string publicKey *ecdsa.PublicKey } // A generic EC-based decrypter/signer type ecDecrypterSigner struct { privateKey *ecdsa.PrivateKey } // newRSARecipient creates recipientKeyInfo based on the given key. func newRSARecipient(keyAlg KeyAlgorithm, publicKey *rsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case RSA1_5, RSA_OAEP, RSA_OAEP_256: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &rsaEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newRSASigner creates a recipientSigInfo based on the given key. func newRSASigner(sigAlg SignatureAlgorithm, privateKey *rsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case RS256, RS384, RS512, PS256, PS384, PS512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } return recipientSigInfo{ sigAlg: sigAlg, publicKey: &JsonWebKey{ Key: &privateKey.PublicKey, }, signer: &rsaDecrypterSigner{ privateKey: privateKey, }, }, nil } // newECDHRecipient creates recipientKeyInfo based on the given key. func newECDHRecipient(keyAlg KeyAlgorithm, publicKey *ecdsa.PublicKey) (recipientKeyInfo, error) { // Verify that key management algorithm is supported by this encrypter switch keyAlg { case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientKeyInfo{}, ErrUnsupportedAlgorithm } return recipientKeyInfo{ keyAlg: keyAlg, keyEncrypter: &ecEncrypterVerifier{ publicKey: publicKey, }, }, nil } // newECDSASigner creates a recipientSigInfo based on the given key. func newECDSASigner(sigAlg SignatureAlgorithm, privateKey *ecdsa.PrivateKey) (recipientSigInfo, error) { // Verify that key management algorithm is supported by this encrypter switch sigAlg { case ES256, ES384, ES512: default: return recipientSigInfo{}, ErrUnsupportedAlgorithm } return recipientSigInfo{ sigAlg: sigAlg, publicKey: &JsonWebKey{ Key: &privateKey.PublicKey, }, signer: &ecDecrypterSigner{ privateKey: privateKey, }, }, nil } // Encrypt the given payload and update the object. func (ctx rsaEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { encryptedKey, err := ctx.encrypt(cek, alg) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: encryptedKey, header: &rawHeader{}, }, nil } // Encrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaEncrypterVerifier) encrypt(cek []byte, alg KeyAlgorithm) ([]byte, error) { switch alg { case RSA1_5: return rsa.EncryptPKCS1v15(randReader, ctx.publicKey, cek) case RSA_OAEP: return rsa.EncryptOAEP(sha1.New(), randReader, ctx.publicKey, cek, []byte{}) case RSA_OAEP_256: return rsa.EncryptOAEP(sha256.New(), randReader, ctx.publicKey, cek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Decrypt the given payload and return the content encryption key. func (ctx rsaDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { return ctx.decrypt(recipient.encryptedKey, KeyAlgorithm(headers.Alg), generator) } // Decrypt the given payload. Based on the key encryption algorithm, // this will either use RSA-PKCS1v1.5 or RSA-OAEP (with SHA-1 or SHA-256). func (ctx rsaDecrypterSigner) decrypt(jek []byte, alg KeyAlgorithm, generator keyGenerator) ([]byte, error) { // Note: The random reader on decrypt operations is only used for blinding, // so stubbing is meanlingless (hence the direct use of rand.Reader). switch alg { case RSA1_5: defer func() { // DecryptPKCS1v15SessionKey sometimes panics on an invalid payload // because of an index out of bounds error, which we want to ignore. // This has been fixed in Go 1.3.1 (released 2014/08/13), the recover() // only exists for preventing crashes with unpatched versions. // See: https://groups.google.com/forum/#!topic/golang-dev/7ihX6Y6kx9k // See: https://code.google.com/p/go/source/detail?r=58ee390ff31602edb66af41ed10901ec95904d33 _ = recover() }() // Perform some input validation. keyBytes := ctx.privateKey.PublicKey.N.BitLen() / 8 if keyBytes != len(jek) { // Input size is incorrect, the encrypted payload should always match // the size of the public modulus (e.g. using a 2048 bit key will // produce 256 bytes of output). Reject this since it's invalid input. return nil, ErrCryptoFailure } cek, _, err := generator.genKey() if err != nil { return nil, ErrCryptoFailure } // When decrypting an RSA-PKCS1v1.5 payload, we must take precautions to // prevent chosen-ciphertext attacks as described in RFC 3218, "Preventing // the Million Message Attack on Cryptographic Message Syntax". We are // therefore deliberatly ignoring errors here. _ = rsa.DecryptPKCS1v15SessionKey(rand.Reader, ctx.privateKey, jek, cek) return cek, nil case RSA_OAEP: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha1.New(), rand.Reader, ctx.privateKey, jek, []byte{}) case RSA_OAEP_256: // Use rand.Reader for RSA blinding return rsa.DecryptOAEP(sha256.New(), rand.Reader, ctx.privateKey, jek, []byte{}) } return nil, ErrUnsupportedAlgorithm } // Sign the given payload func (ctx rsaDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return Signature{}, ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) var out []byte var err error switch alg { case RS256, RS384, RS512: out, err = rsa.SignPKCS1v15(randReader, ctx.privateKey, hash, hashed) case PS256, PS384, PS512: out, err = rsa.SignPSS(randReader, ctx.privateKey, hash, hashed, &rsa.PSSOptions{ SaltLength: rsa.PSSSaltLengthAuto, }) } if err != nil { return Signature{}, err } return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx rsaEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var hash crypto.Hash switch alg { case RS256, PS256: hash = crypto.SHA256 case RS384, PS384: hash = crypto.SHA384 case RS512, PS512: hash = crypto.SHA512 default: return ErrUnsupportedAlgorithm } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) switch alg { case RS256, RS384, RS512: return rsa.VerifyPKCS1v15(ctx.publicKey, hash, hashed, signature) case PS256, PS384, PS512: return rsa.VerifyPSS(ctx.publicKey, hash, hashed, signature, nil) } return ErrUnsupportedAlgorithm } // Encrypt the given payload and update the object. func (ctx ecEncrypterVerifier) encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) { switch alg { case ECDH_ES: // ECDH-ES mode doesn't wrap a key, the shared secret is used directly as the key. return recipientInfo{ header: &rawHeader{}, }, nil case ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: default: return recipientInfo{}, ErrUnsupportedAlgorithm } generator := ecKeyGenerator{ algID: string(alg), publicKey: ctx.publicKey, } switch alg { case ECDH_ES_A128KW: generator.size = 16 case ECDH_ES_A192KW: generator.size = 24 case ECDH_ES_A256KW: generator.size = 32 } kek, header, err := generator.genKey() if err != nil { return recipientInfo{}, err } block, err := aes.NewCipher(kek) if err != nil { return recipientInfo{}, err } jek, err := josecipher.KeyWrap(block, cek) if err != nil { return recipientInfo{}, err } return recipientInfo{ encryptedKey: jek, header: &header, }, nil } // Get key size for EC key generator func (ctx ecKeyGenerator) keySize() int { return ctx.size } // Get a content encryption key for ECDH-ES func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) { priv, err := ecdsa.GenerateKey(ctx.publicKey.Curve, randReader) if err != nil { return nil, rawHeader{}, err } out := josecipher.DeriveECDHES(ctx.algID, []byte{}, []byte{}, priv, ctx.publicKey, ctx.size) headers := rawHeader{ Epk: &JsonWebKey{ Key: &priv.PublicKey, }, } return out, headers, nil } // Decrypt the given payload and return the content encryption key. func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) { if headers.Epk == nil { return nil, errors.New("square/go-jose: missing epk header") } publicKey, ok := headers.Epk.Key.(*ecdsa.PublicKey) if publicKey == nil || !ok { return nil, errors.New("square/go-jose: invalid epk header") } apuData := headers.Apu.bytes() apvData := headers.Apv.bytes() deriveKey := func(algID string, size int) []byte { return josecipher.DeriveECDHES(algID, apuData, apvData, ctx.privateKey, publicKey, size) } var keySize int switch KeyAlgorithm(headers.Alg) { case ECDH_ES: // ECDH-ES uses direct key agreement, no key unwrapping necessary. return deriveKey(string(headers.Enc), generator.keySize()), nil case ECDH_ES_A128KW: keySize = 16 case ECDH_ES_A192KW: keySize = 24 case ECDH_ES_A256KW: keySize = 32 default: return nil, ErrUnsupportedAlgorithm } key := deriveKey(headers.Alg, keySize) block, err := aes.NewCipher(key) if err != nil { return nil, err } return josecipher.KeyUnwrap(block, recipient.encryptedKey) } // Sign the given payload func (ctx ecDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) { var expectedBitSize int var hash crypto.Hash switch alg { case ES256: expectedBitSize = 256 hash = crypto.SHA256 case ES384: expectedBitSize = 384 hash = crypto.SHA384 case ES512: expectedBitSize = 521 hash = crypto.SHA512 } curveBits := ctx.privateKey.Curve.Params().BitSize if expectedBitSize != curveBits { return Signature{}, fmt.Errorf("square/go-jose: expected %d bit key, got %d bits instead", expectedBitSize, curveBits) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r, s, err := ecdsa.Sign(randReader, ctx.privateKey, hashed) if err != nil { return Signature{}, err } keyBytes := curveBits / 8 if curveBits%8 > 0 { keyBytes += 1 } // We serialize the outpus (r and s) into big-endian byte arrays and pad // them with zeros on the left to make sure the sizes work out. Both arrays // must be keyBytes long, and the output must be 2*keyBytes long. rBytes := r.Bytes() rBytesPadded := make([]byte, keyBytes) copy(rBytesPadded[keyBytes-len(rBytes):], rBytes) sBytes := s.Bytes() sBytesPadded := make([]byte, keyBytes) copy(sBytesPadded[keyBytes-len(sBytes):], sBytes) out := append(rBytesPadded, sBytesPadded...) return Signature{ Signature: out, protected: &rawHeader{}, }, nil } // Verify the given payload func (ctx ecEncrypterVerifier) verifyPayload(payload []byte, signature []byte, alg SignatureAlgorithm) error { var keySize int var hash crypto.Hash switch alg { case ES256: keySize = 32 hash = crypto.SHA256 case ES384: keySize = 48 hash = crypto.SHA384 case ES512: keySize = 66 hash = crypto.SHA512 } if len(signature) != 2*keySize { return fmt.Errorf("square/go-jose: invalid signature size, have %d bytes, wanted %d", len(signature), 2*keySize) } hasher := hash.New() // According to documentation, Write() on hash never fails _, _ = hasher.Write(payload) hashed := hasher.Sum(nil) r := big.NewInt(0).SetBytes(signature[:keySize]) s := big.NewInt(0).SetBytes(signature[keySize:]) match := ecdsa.Verify(ctx.publicKey, hashed, r, s) if !match { return errors.New("square/go-jose: ecdsa signature failed to verify") } return nil } asymmetric_test.go000066400000000000000000000256721313450123100344040ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "crypto/rand" "crypto/rsa" "errors" "io" "math/big" "testing" ) func TestVectorsRSA(t *testing.T) { // Sources: // http://www.emc.com/emc-plus/rsa-labs/standards-initiatives/pkcs-rsa-cryptography-standard.htm // ftp://ftp.rsa.com/pub/rsalabs/tmp/pkcs1v15crypt-vectors.txt priv := &rsa.PrivateKey{ PublicKey: rsa.PublicKey{ N: fromHexInt(` a8b3b284af8eb50b387034a860f146c4919f318763cd6c5598c8 ae4811a1e0abc4c7e0b082d693a5e7fced675cf4668512772c0c bc64a742c6c630f533c8cc72f62ae833c40bf25842e984bb78bd bf97c0107d55bdb662f5c4e0fab9845cb5148ef7392dd3aaff93 ae1e6b667bb3d4247616d4f5ba10d4cfd226de88d39f16fb`), E: 65537, }, D: fromHexInt(` 53339cfdb79fc8466a655c7316aca85c55fd8f6dd898fdaf1195 17ef4f52e8fd8e258df93fee180fa0e4ab29693cd83b152a553d 4ac4d1812b8b9fa5af0e7f55fe7304df41570926f3311f15c4d6 5a732c483116ee3d3d2d0af3549ad9bf7cbfb78ad884f84d5beb 04724dc7369b31def37d0cf539e9cfcdd3de653729ead5d1`), Primes: []*big.Int{ fromHexInt(` d32737e7267ffe1341b2d5c0d150a81b586fb3132bed2f8d5262 864a9cb9f30af38be448598d413a172efb802c21acf1c11c520c 2f26a471dcad212eac7ca39d`), fromHexInt(` cc8853d1d54da630fac004f471f281c7b8982d8224a490edbeb3 3d3e3d5cc93c4765703d1dd791642f1f116a0dd852be2419b2af 72bfe9a030e860b0288b5d77`), }, } input := fromHexBytes( "6628194e12073db03ba94cda9ef9532397d50dba79b987004afefe34") expectedPKCS := fromHexBytes(` 50b4c14136bd198c2f3c3ed243fce036e168d56517984a263cd66492b808 04f169d210f2b9bdfb48b12f9ea05009c77da257cc600ccefe3a6283789d 8ea0e607ac58e2690ec4ebc10146e8cbaa5ed4d5cce6fe7b0ff9efc1eabb 564dbf498285f449ee61dd7b42ee5b5892cb90601f30cda07bf26489310b cd23b528ceab3c31`) expectedOAEP := fromHexBytes(` 354fe67b4a126d5d35fe36c777791a3f7ba13def484e2d3908aff722fad4 68fb21696de95d0be911c2d3174f8afcc201035f7b6d8e69402de5451618 c21a535fa9d7bfc5b8dd9fc243f8cf927db31322d6e881eaa91a996170e6 57a05a266426d98c88003f8477c1227094a0d9fa1e8c4024309ce1ecccb5 210035d47ac72e8a`) // Mock random reader randReader = bytes.NewReader(fromHexBytes(` 017341ae3875d5f87101f8cc4fa9b9bc156bb04628fccdb2f4f11e905bd3 a155d376f593bd7304210874eba08a5e22bcccb4c9d3882a93a54db022f5 03d16338b6b7ce16dc7f4bbf9a96b59772d6606e9747c7649bf9e083db98 1884a954ab3c6f18b776ea21069d69776a33e96bad48e1dda0a5ef`)) defer resetRandReader() // RSA-PKCS1v1.5 encrypt enc := new(rsaEncrypterVerifier) enc.publicKey = &priv.PublicKey encryptedPKCS, err := enc.encrypt(input, RSA1_5) if err != nil { t.Error("Encryption failed:", err) return } if bytes.Compare(encryptedPKCS, expectedPKCS) != 0 { t.Error("Output does not match expected value (PKCS1v1.5)") } // RSA-OAEP encrypt encryptedOAEP, err := enc.encrypt(input, RSA_OAEP) if err != nil { t.Error("Encryption failed:", err) return } if bytes.Compare(encryptedOAEP, expectedOAEP) != 0 { t.Error("Output does not match expected value (OAEP)") } // Need fake cipher for PKCS1v1.5 decrypt resetRandReader() aes := newAESGCM(len(input)) keygen := randomKeyGenerator{ size: aes.keySize(), } // RSA-PKCS1v1.5 decrypt dec := new(rsaDecrypterSigner) dec.privateKey = priv decryptedPKCS, err := dec.decrypt(encryptedPKCS, RSA1_5, keygen) if err != nil { t.Error("Decryption failed:", err) return } if bytes.Compare(input, decryptedPKCS) != 0 { t.Error("Output does not match expected value (PKCS1v1.5)") } // RSA-OAEP decrypt decryptedOAEP, err := dec.decrypt(encryptedOAEP, RSA_OAEP, keygen) if err != nil { t.Error("decryption failed:", err) return } if bytes.Compare(input, decryptedOAEP) != 0 { t.Error("output does not match expected value (OAEP)") } } func TestInvalidAlgorithmsRSA(t *testing.T) { _, err := newRSARecipient("XYZ", nil) if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } _, err = newRSASigner("XYZ", nil) if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } enc := new(rsaEncrypterVerifier) enc.publicKey = &rsaTestKey.PublicKey _, err = enc.encryptKey([]byte{}, "XYZ") if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } err = enc.verifyPayload([]byte{}, []byte{}, "XYZ") if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } dec := new(rsaDecrypterSigner) dec.privateKey = rsaTestKey _, err = dec.decrypt(make([]byte, 256), "XYZ", randomKeyGenerator{size: 16}) if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } _, err = dec.signPayload([]byte{}, "XYZ") if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } } type failingKeyGenerator struct{} func (ctx failingKeyGenerator) keySize() int { return 0 } func (ctx failingKeyGenerator) genKey() ([]byte, rawHeader, error) { return nil, rawHeader{}, errors.New("failed to generate key") } func TestPKCSKeyGeneratorFailure(t *testing.T) { dec := new(rsaDecrypterSigner) dec.privateKey = rsaTestKey generator := failingKeyGenerator{} _, err := dec.decrypt(make([]byte, 256), RSA1_5, generator) if err != ErrCryptoFailure { t.Error("should return error on invalid algorithm") } } func TestInvalidAlgorithmsEC(t *testing.T) { _, err := newECDHRecipient("XYZ", nil) if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } _, err = newECDSASigner("XYZ", nil) if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } enc := new(ecEncrypterVerifier) enc.publicKey = &ecTestKey256.PublicKey _, err = enc.encryptKey([]byte{}, "XYZ") if err != ErrUnsupportedAlgorithm { t.Error("should return error on invalid algorithm") } } func TestInvalidECKeyGen(t *testing.T) { gen := ecKeyGenerator{ size: 16, algID: "A128GCM", publicKey: &ecTestKey256.PublicKey, } if gen.keySize() != 16 { t.Error("ec key generator reported incorrect key size") } _, _, err := gen.genKey() if err != nil { t.Error("ec key generator failed to generate key", err) } } func TestInvalidECDecrypt(t *testing.T) { dec := ecDecrypterSigner{ privateKey: ecTestKey256, } generator := randomKeyGenerator{size: 16} // Missing epk header headers := rawHeader{ Alg: string(ECDH_ES), } _, err := dec.decryptKey(headers, nil, generator) if err == nil { t.Error("ec decrypter accepted object with missing epk header") } // Invalid epk header headers.Epk = &JsonWebKey{} _, err = dec.decryptKey(headers, nil, generator) if err == nil { t.Error("ec decrypter accepted object with invalid epk header") } } func TestDecryptWithIncorrectSize(t *testing.T) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Error(err) return } dec := new(rsaDecrypterSigner) dec.privateKey = priv aes := newAESGCM(16) keygen := randomKeyGenerator{ size: aes.keySize(), } payload := make([]byte, 254) _, err = dec.decrypt(payload, RSA1_5, keygen) if err == nil { t.Error("Invalid payload size should return error") } payload = make([]byte, 257) _, err = dec.decrypt(payload, RSA1_5, keygen) if err == nil { t.Error("Invalid payload size should return error") } } func TestPKCSDecryptNeverFails(t *testing.T) { // We don't want RSA-PKCS1 v1.5 decryption to ever fail, in order to prevent // side-channel timing attacks (Bleichenbacher attack in particular). priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Error(err) return } dec := new(rsaDecrypterSigner) dec.privateKey = priv aes := newAESGCM(16) keygen := randomKeyGenerator{ size: aes.keySize(), } for i := 1; i < 50; i++ { payload := make([]byte, 256) _, err := io.ReadFull(rand.Reader, payload) if err != nil { t.Error("Unable to get random data:", err) return } _, err = dec.decrypt(payload, RSA1_5, keygen) if err != nil { t.Error("PKCS1v1.5 decrypt should never fail:", err) return } } } func BenchmarkPKCSDecryptWithValidPayloads(b *testing.B) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } enc := new(rsaEncrypterVerifier) enc.publicKey = &priv.PublicKey dec := new(rsaDecrypterSigner) dec.privateKey = priv aes := newAESGCM(32) b.StopTimer() b.ResetTimer() for i := 0; i < b.N; i++ { plaintext := make([]byte, 32) _, err = io.ReadFull(rand.Reader, plaintext) if err != nil { panic(err) } ciphertext, err := enc.encrypt(plaintext, RSA1_5) if err != nil { panic(err) } keygen := randomKeyGenerator{ size: aes.keySize(), } b.StartTimer() _, err = dec.decrypt(ciphertext, RSA1_5, keygen) b.StopTimer() if err != nil { panic(err) } } } func BenchmarkPKCSDecryptWithInvalidPayloads(b *testing.B) { priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } enc := new(rsaEncrypterVerifier) enc.publicKey = &priv.PublicKey dec := new(rsaDecrypterSigner) dec.privateKey = priv aes := newAESGCM(16) keygen := randomKeyGenerator{ size: aes.keySize(), } b.StopTimer() b.ResetTimer() for i := 0; i < b.N; i++ { plaintext := make([]byte, 16) _, err = io.ReadFull(rand.Reader, plaintext) if err != nil { panic(err) } ciphertext, err := enc.encrypt(plaintext, RSA1_5) if err != nil { panic(err) } // Do some simple scrambling ciphertext[128] ^= 0xFF b.StartTimer() _, err = dec.decrypt(ciphertext, RSA1_5, keygen) b.StopTimer() if err != nil { panic(err) } } } func TestInvalidEllipticCurve(t *testing.T) { signer256 := ecDecrypterSigner{privateKey: ecTestKey256} signer384 := ecDecrypterSigner{privateKey: ecTestKey384} signer521 := ecDecrypterSigner{privateKey: ecTestKey521} _, err := signer256.signPayload([]byte{}, ES384) if err == nil { t.Error("should not generate ES384 signature with P-256 key") } _, err = signer256.signPayload([]byte{}, ES512) if err == nil { t.Error("should not generate ES512 signature with P-256 key") } _, err = signer384.signPayload([]byte{}, ES256) if err == nil { t.Error("should not generate ES256 signature with P-384 key") } _, err = signer384.signPayload([]byte{}, ES512) if err == nil { t.Error("should not generate ES512 signature with P-384 key") } _, err = signer521.signPayload([]byte{}, ES256) if err == nil { t.Error("should not generate ES256 signature with P-521 key") } _, err = signer521.signPayload([]byte{}, ES384) if err == nil { t.Error("should not generate ES384 signature with P-521 key") } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/000077500000000000000000000000001313450123100321565ustar00rootroot00000000000000cbc_hmac.go000066400000000000000000000122631313450123100341510ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/cipher" "crypto/hmac" "crypto/sha256" "crypto/sha512" "crypto/subtle" "encoding/binary" "errors" "hash" ) const ( nonceBytes = 16 ) // NewCBCHMAC instantiates a new AEAD based on CBC+HMAC. func NewCBCHMAC(key []byte, newBlockCipher func([]byte) (cipher.Block, error)) (cipher.AEAD, error) { keySize := len(key) / 2 integrityKey := key[:keySize] encryptionKey := key[keySize:] blockCipher, err := newBlockCipher(encryptionKey) if err != nil { return nil, err } var hash func() hash.Hash switch keySize { case 16: hash = sha256.New case 24: hash = sha512.New384 case 32: hash = sha512.New } return &cbcAEAD{ hash: hash, blockCipher: blockCipher, authtagBytes: keySize, integrityKey: integrityKey, }, nil } // An AEAD based on CBC+HMAC type cbcAEAD struct { hash func() hash.Hash authtagBytes int integrityKey []byte blockCipher cipher.Block } func (ctx *cbcAEAD) NonceSize() int { return nonceBytes } func (ctx *cbcAEAD) Overhead() int { // Maximum overhead is block size (for padding) plus auth tag length, where // the length of the auth tag is equivalent to the key size. return ctx.blockCipher.BlockSize() + ctx.authtagBytes } // Seal encrypts and authenticates the plaintext. func (ctx *cbcAEAD) Seal(dst, nonce, plaintext, data []byte) []byte { // Output buffer -- must take care not to mangle plaintext input. ciphertext := make([]byte, len(plaintext)+ctx.Overhead())[:len(plaintext)] copy(ciphertext, plaintext) ciphertext = padBuffer(ciphertext, ctx.blockCipher.BlockSize()) cbc := cipher.NewCBCEncrypter(ctx.blockCipher, nonce) cbc.CryptBlocks(ciphertext, ciphertext) authtag := ctx.computeAuthTag(data, nonce, ciphertext) ret, out := resize(dst, len(dst)+len(ciphertext)+len(authtag)) copy(out, ciphertext) copy(out[len(ciphertext):], authtag) return ret } // Open decrypts and authenticates the ciphertext. func (ctx *cbcAEAD) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) { if len(ciphertext) < ctx.authtagBytes { return nil, errors.New("square/go-jose: invalid ciphertext (too short)") } offset := len(ciphertext) - ctx.authtagBytes expectedTag := ctx.computeAuthTag(data, nonce, ciphertext[:offset]) match := subtle.ConstantTimeCompare(expectedTag, ciphertext[offset:]) if match != 1 { return nil, errors.New("square/go-jose: invalid ciphertext (auth tag mismatch)") } cbc := cipher.NewCBCDecrypter(ctx.blockCipher, nonce) // Make copy of ciphertext buffer, don't want to modify in place buffer := append([]byte{}, []byte(ciphertext[:offset])...) if len(buffer)%ctx.blockCipher.BlockSize() > 0 { return nil, errors.New("square/go-jose: invalid ciphertext (invalid length)") } cbc.CryptBlocks(buffer, buffer) // Remove padding plaintext, err := unpadBuffer(buffer, ctx.blockCipher.BlockSize()) if err != nil { return nil, err } ret, out := resize(dst, len(dst)+len(plaintext)) copy(out, plaintext) return ret, nil } // Compute an authentication tag func (ctx *cbcAEAD) computeAuthTag(aad, nonce, ciphertext []byte) []byte { buffer := make([]byte, len(aad)+len(nonce)+len(ciphertext)+8) n := 0 n += copy(buffer, aad) n += copy(buffer[n:], nonce) n += copy(buffer[n:], ciphertext) binary.BigEndian.PutUint64(buffer[n:], uint64(len(aad)*8)) // According to documentation, Write() on hash.Hash never fails. hmac := hmac.New(ctx.hash, ctx.integrityKey) _, _ = hmac.Write(buffer) return hmac.Sum(nil)[:ctx.authtagBytes] } // resize ensures the the given slice has a capacity of at least n bytes. // If the capacity of the slice is less than n, a new slice is allocated // and the existing data will be copied. func resize(in []byte, n int) (head, tail []byte) { if cap(in) >= n { head = in[:n] } else { head = make([]byte, n) copy(head, in) } tail = head[len(in):] return } // Apply padding func padBuffer(buffer []byte, blockSize int) []byte { missing := blockSize - (len(buffer) % blockSize) ret, out := resize(buffer, len(buffer)+missing) padding := bytes.Repeat([]byte{byte(missing)}, missing) copy(out, padding) return ret } // Remove padding func unpadBuffer(buffer []byte, blockSize int) ([]byte, error) { if len(buffer)%blockSize != 0 { return nil, errors.New("square/go-jose: invalid padding") } last := buffer[len(buffer)-1] count := int(last) if count == 0 || count > blockSize || count > len(buffer) { return nil, errors.New("square/go-jose: invalid padding") } padding := bytes.Repeat([]byte{last}, count) if !bytes.HasSuffix(buffer, padding) { return nil, errors.New("square/go-jose: invalid padding") } return buffer[:len(buffer)-count], nil } cbc_hmac_test.go000066400000000000000000000344641313450123100352170ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/rand" "io" "strings" "testing" ) func TestInvalidInputs(t *testing.T) { key := []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, } nonce := []byte{ 92, 80, 104, 49, 133, 25, 161, 215, 173, 101, 219, 211, 136, 91, 210, 145} aead, _ := NewCBCHMAC(key, aes.NewCipher) ciphertext := aead.Seal(nil, nonce, []byte("plaintext"), []byte("aad")) // Changed AAD, must fail _, err := aead.Open(nil, nonce, ciphertext, []byte("INVALID")) if err == nil { t.Error("must detect invalid aad") } // Empty ciphertext, must fail _, err = aead.Open(nil, nonce, []byte{}, []byte("aad")) if err == nil { t.Error("must detect invalid/empty ciphertext") } // Corrupt ciphertext, must fail corrupt := make([]byte, len(ciphertext)) copy(corrupt, ciphertext) corrupt[0] ^= 0xFF _, err = aead.Open(nil, nonce, corrupt, []byte("aad")) if err == nil { t.Error("must detect corrupt ciphertext") } // Corrupt authtag, must fail copy(corrupt, ciphertext) corrupt[len(ciphertext)-1] ^= 0xFF _, err = aead.Open(nil, nonce, corrupt, []byte("aad")) if err == nil { t.Error("must detect corrupt authtag") } // Truncated data, must fail _, err = aead.Open(nil, nonce, ciphertext[:10], []byte("aad")) if err == nil { t.Error("must detect corrupt authtag") } } func TestVectorsAESCBC128(t *testing.T) { // Source: http://tools.ietf.org/html/draft-ietf-jose-json-web-encryption-29#appendix-A.2 plaintext := []byte{ 76, 105, 118, 101, 32, 108, 111, 110, 103, 32, 97, 110, 100, 32, 112, 114, 111, 115, 112, 101, 114, 46} aad := []byte{ 101, 121, 74, 104, 98, 71, 99, 105, 79, 105, 74, 83, 85, 48, 69, 120, 88, 122, 85, 105, 76, 67, 74, 108, 98, 109, 77, 105, 79, 105, 74, 66, 77, 84, 73, 52, 81, 48, 74, 68, 76, 85, 104, 84, 77, 106, 85, 50, 73, 110, 48} expectedCiphertext := []byte{ 40, 57, 83, 181, 119, 33, 133, 148, 198, 185, 243, 24, 152, 230, 6, 75, 129, 223, 127, 19, 210, 82, 183, 230, 168, 33, 215, 104, 143, 112, 56, 102} expectedAuthtag := []byte{ 246, 17, 244, 190, 4, 95, 98, 3, 231, 0, 115, 157, 242, 203, 100, 191} key := []byte{ 4, 211, 31, 197, 84, 157, 252, 254, 11, 100, 157, 250, 63, 170, 106, 206, 107, 124, 212, 45, 111, 107, 9, 219, 200, 177, 0, 240, 143, 156, 44, 207} nonce := []byte{ 3, 22, 60, 12, 43, 67, 104, 105, 108, 108, 105, 99, 111, 116, 104, 101} enc, err := NewCBCHMAC(key, aes.NewCipher) out := enc.Seal(nil, nonce, plaintext, aad) if err != nil { t.Error("Unable to encrypt:", err) return } if bytes.Compare(out[:len(out)-16], expectedCiphertext) != 0 { t.Error("Ciphertext did not match") } if bytes.Compare(out[len(out)-16:], expectedAuthtag) != 0 { t.Error("Auth tag did not match") } } func TestVectorsAESCBC256(t *testing.T) { // Source: https://tools.ietf.org/html/draft-mcgrew-aead-aes-cbc-hmac-sha2-05#section-5.4 plaintext := []byte{ 0x41, 0x20, 0x63, 0x69, 0x70, 0x68, 0x65, 0x72, 0x20, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x6e, 0x6f, 0x74, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x2c, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x62, 0x6c, 0x65, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x61, 0x6c, 0x6c, 0x20, 0x69, 0x6e, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x68, 0x61, 0x6e, 0x64, 0x73, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x65, 0x6e, 0x65, 0x6d, 0x79, 0x20, 0x77, 0x69, 0x74, 0x68, 0x6f, 0x75, 0x74, 0x20, 0x69, 0x6e, 0x63, 0x6f, 0x6e, 0x76, 0x65, 0x6e, 0x69, 0x65, 0x6e, 0x63, 0x65} aad := []byte{ 0x54, 0x68, 0x65, 0x20, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x20, 0x70, 0x72, 0x69, 0x6e, 0x63, 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x41, 0x75, 0x67, 0x75, 0x73, 0x74, 0x65, 0x20, 0x4b, 0x65, 0x72, 0x63, 0x6b, 0x68, 0x6f, 0x66, 0x66, 0x73} expectedCiphertext := []byte{ 0x4a, 0xff, 0xaa, 0xad, 0xb7, 0x8c, 0x31, 0xc5, 0xda, 0x4b, 0x1b, 0x59, 0x0d, 0x10, 0xff, 0xbd, 0x3d, 0xd8, 0xd5, 0xd3, 0x02, 0x42, 0x35, 0x26, 0x91, 0x2d, 0xa0, 0x37, 0xec, 0xbc, 0xc7, 0xbd, 0x82, 0x2c, 0x30, 0x1d, 0xd6, 0x7c, 0x37, 0x3b, 0xcc, 0xb5, 0x84, 0xad, 0x3e, 0x92, 0x79, 0xc2, 0xe6, 0xd1, 0x2a, 0x13, 0x74, 0xb7, 0x7f, 0x07, 0x75, 0x53, 0xdf, 0x82, 0x94, 0x10, 0x44, 0x6b, 0x36, 0xeb, 0xd9, 0x70, 0x66, 0x29, 0x6a, 0xe6, 0x42, 0x7e, 0xa7, 0x5c, 0x2e, 0x08, 0x46, 0xa1, 0x1a, 0x09, 0xcc, 0xf5, 0x37, 0x0d, 0xc8, 0x0b, 0xfe, 0xcb, 0xad, 0x28, 0xc7, 0x3f, 0x09, 0xb3, 0xa3, 0xb7, 0x5e, 0x66, 0x2a, 0x25, 0x94, 0x41, 0x0a, 0xe4, 0x96, 0xb2, 0xe2, 0xe6, 0x60, 0x9e, 0x31, 0xe6, 0xe0, 0x2c, 0xc8, 0x37, 0xf0, 0x53, 0xd2, 0x1f, 0x37, 0xff, 0x4f, 0x51, 0x95, 0x0b, 0xbe, 0x26, 0x38, 0xd0, 0x9d, 0xd7, 0xa4, 0x93, 0x09, 0x30, 0x80, 0x6d, 0x07, 0x03, 0xb1, 0xf6} expectedAuthtag := []byte{ 0x4d, 0xd3, 0xb4, 0xc0, 0x88, 0xa7, 0xf4, 0x5c, 0x21, 0x68, 0x39, 0x64, 0x5b, 0x20, 0x12, 0xbf, 0x2e, 0x62, 0x69, 0xa8, 0xc5, 0x6a, 0x81, 0x6d, 0xbc, 0x1b, 0x26, 0x77, 0x61, 0x95, 0x5b, 0xc5} key := []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f} nonce := []byte{ 0x1a, 0xf3, 0x8c, 0x2d, 0xc2, 0xb9, 0x6f, 0xfd, 0xd8, 0x66, 0x94, 0x09, 0x23, 0x41, 0xbc, 0x04} enc, err := NewCBCHMAC(key, aes.NewCipher) out := enc.Seal(nil, nonce, plaintext, aad) if err != nil { t.Error("Unable to encrypt:", err) return } if bytes.Compare(out[:len(out)-32], expectedCiphertext) != 0 { t.Error("Ciphertext did not match, got", out[:len(out)-32], "wanted", expectedCiphertext) } if bytes.Compare(out[len(out)-32:], expectedAuthtag) != 0 { t.Error("Auth tag did not match, got", out[len(out)-32:], "wanted", expectedAuthtag) } } func TestAESCBCRoundtrip(t *testing.T) { key128 := []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} key192 := []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7} key256 := []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} nonce := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15} RunRoundtrip(t, key128, nonce) RunRoundtrip(t, key192, nonce) RunRoundtrip(t, key256, nonce) } func RunRoundtrip(t *testing.T, key, nonce []byte) { aead, err := NewCBCHMAC(key, aes.NewCipher) if err != nil { panic(err) } if aead.NonceSize() != len(nonce) { panic("invalid nonce") } // Test pre-existing data in dst buffer dst := []byte{15, 15, 15, 15} plaintext := []byte{0, 0, 0, 0} aad := []byte{4, 3, 2, 1} result := aead.Seal(dst, nonce, plaintext, aad) if bytes.Compare(dst, result[:4]) != 0 { t.Error("Existing data in dst not preserved") } // Test pre-existing (empty) dst buffer with sufficient capacity dst = make([]byte, 256)[:0] result, err = aead.Open(dst, nonce, result[4:], aad) if err != nil { panic(err) } if bytes.Compare(result, plaintext) != 0 { t.Error("Plaintext does not match output") } } func TestAESCBCOverhead(t *testing.T) { aead, err := NewCBCHMAC(make([]byte, 32), aes.NewCipher) if err != nil { panic(err) } if aead.Overhead() != 32 { t.Error("CBC-HMAC reports incorrect overhead value") } } func TestPadding(t *testing.T) { for i := 0; i < 256; i++ { slice := make([]byte, i) padded := padBuffer(slice, 16) if len(padded)%16 != 0 { t.Error("failed to pad slice properly", i) return } unpadded, err := unpadBuffer(padded, 16) if err != nil || len(unpadded) != i { t.Error("failed to unpad slice properly", i) return } } } func TestInvalidKey(t *testing.T) { key := make([]byte, 30) _, err := NewCBCHMAC(key, aes.NewCipher) if err == nil { t.Error("should not be able to instantiate CBC-HMAC with invalid key") } } func TestTruncatedCiphertext(t *testing.T) { key := make([]byte, 32) nonce := make([]byte, 16) data := make([]byte, 32) io.ReadFull(rand.Reader, key) io.ReadFull(rand.Reader, nonce) aead, err := NewCBCHMAC(key, aes.NewCipher) if err != nil { panic(err) } ctx := aead.(*cbcAEAD) ct := aead.Seal(nil, nonce, data, nil) // Truncated ciphertext, but with correct auth tag truncated, tail := resize(ct[:len(ct)-ctx.authtagBytes-2], len(ct)-2) copy(tail, ctx.computeAuthTag(nil, nonce, truncated[:len(truncated)-ctx.authtagBytes])) // Open should fail _, err = aead.Open(nil, nonce, truncated, nil) if err == nil { t.Error("open on truncated ciphertext should fail") } } func TestInvalidPaddingOpen(t *testing.T) { key := make([]byte, 32) nonce := make([]byte, 16) // Plaintext with invalid padding plaintext := padBuffer(make([]byte, 28), aes.BlockSize) plaintext[len(plaintext)-1] = 0xFF io.ReadFull(rand.Reader, key) io.ReadFull(rand.Reader, nonce) block, _ := aes.NewCipher(key) cbc := cipher.NewCBCEncrypter(block, nonce) buffer := append([]byte{}, plaintext...) cbc.CryptBlocks(buffer, buffer) aead, _ := NewCBCHMAC(key, aes.NewCipher) ctx := aead.(*cbcAEAD) // Mutated ciphertext, but with correct auth tag size := len(buffer) ciphertext, tail := resize(buffer, size+(len(key)/2)) copy(tail, ctx.computeAuthTag(nil, nonce, ciphertext[:size])) // Open should fail (b/c of invalid padding, even though tag matches) _, err := aead.Open(nil, nonce, ciphertext, nil) if err == nil || !strings.Contains(err.Error(), "invalid padding") { t.Error("no or unexpected error on open with invalid padding:", err) } } func TestInvalidPadding(t *testing.T) { for i := 0; i < 256; i++ { slice := make([]byte, i) padded := padBuffer(slice, 16) if len(padded)%16 != 0 { t.Error("failed to pad slice properly", i) return } paddingBytes := 16 - (i % 16) // Mutate padding for testing for j := 1; j <= paddingBytes; j++ { mutated := make([]byte, len(padded)) copy(mutated, padded) mutated[len(mutated)-j] ^= 0xFF _, err := unpadBuffer(mutated, 16) if err == nil { t.Error("unpad on invalid padding should fail", i) return } } // Test truncated padding _, err := unpadBuffer(padded[:len(padded)-1], 16) if err == nil { t.Error("unpad on truncated padding should fail", i) return } } } func TestZeroLengthPadding(t *testing.T) { data := make([]byte, 16) data, err := unpadBuffer(data, 16) if err == nil { t.Error("padding with 0x00 should never be valid") } } func benchEncryptCBCHMAC(b *testing.B, keySize, chunkSize int) { key := make([]byte, keySize*2) nonce := make([]byte, 16) io.ReadFull(rand.Reader, key) io.ReadFull(rand.Reader, nonce) chunk := make([]byte, chunkSize) aead, err := NewCBCHMAC(key, aes.NewCipher) if err != nil { panic(err) } b.SetBytes(int64(chunkSize)) b.ResetTimer() for i := 0; i < b.N; i++ { aead.Seal(nil, nonce, chunk, nil) } } func benchDecryptCBCHMAC(b *testing.B, keySize, chunkSize int) { key := make([]byte, keySize*2) nonce := make([]byte, 16) io.ReadFull(rand.Reader, key) io.ReadFull(rand.Reader, nonce) chunk := make([]byte, chunkSize) aead, err := NewCBCHMAC(key, aes.NewCipher) if err != nil { panic(err) } out := aead.Seal(nil, nonce, chunk, nil) b.SetBytes(int64(chunkSize)) b.ResetTimer() for i := 0; i < b.N; i++ { aead.Open(nil, nonce, out, nil) } } func BenchmarkEncryptAES128_CBCHMAC_1k(b *testing.B) { benchEncryptCBCHMAC(b, 16, 1024) } func BenchmarkEncryptAES128_CBCHMAC_64k(b *testing.B) { benchEncryptCBCHMAC(b, 16, 65536) } func BenchmarkEncryptAES128_CBCHMAC_1MB(b *testing.B) { benchEncryptCBCHMAC(b, 16, 1048576) } func BenchmarkEncryptAES128_CBCHMAC_64MB(b *testing.B) { benchEncryptCBCHMAC(b, 16, 67108864) } func BenchmarkDecryptAES128_CBCHMAC_1k(b *testing.B) { benchDecryptCBCHMAC(b, 16, 1024) } func BenchmarkDecryptAES128_CBCHMAC_64k(b *testing.B) { benchDecryptCBCHMAC(b, 16, 65536) } func BenchmarkDecryptAES128_CBCHMAC_1MB(b *testing.B) { benchDecryptCBCHMAC(b, 16, 1048576) } func BenchmarkDecryptAES128_CBCHMAC_64MB(b *testing.B) { benchDecryptCBCHMAC(b, 16, 67108864) } func BenchmarkEncryptAES192_CBCHMAC_64k(b *testing.B) { benchEncryptCBCHMAC(b, 24, 65536) } func BenchmarkEncryptAES192_CBCHMAC_1MB(b *testing.B) { benchEncryptCBCHMAC(b, 24, 1048576) } func BenchmarkEncryptAES192_CBCHMAC_64MB(b *testing.B) { benchEncryptCBCHMAC(b, 24, 67108864) } func BenchmarkDecryptAES192_CBCHMAC_1k(b *testing.B) { benchDecryptCBCHMAC(b, 24, 1024) } func BenchmarkDecryptAES192_CBCHMAC_64k(b *testing.B) { benchDecryptCBCHMAC(b, 24, 65536) } func BenchmarkDecryptAES192_CBCHMAC_1MB(b *testing.B) { benchDecryptCBCHMAC(b, 24, 1048576) } func BenchmarkDecryptAES192_CBCHMAC_64MB(b *testing.B) { benchDecryptCBCHMAC(b, 24, 67108864) } func BenchmarkEncryptAES256_CBCHMAC_64k(b *testing.B) { benchEncryptCBCHMAC(b, 32, 65536) } func BenchmarkEncryptAES256_CBCHMAC_1MB(b *testing.B) { benchEncryptCBCHMAC(b, 32, 1048576) } func BenchmarkEncryptAES256_CBCHMAC_64MB(b *testing.B) { benchEncryptCBCHMAC(b, 32, 67108864) } func BenchmarkDecryptAES256_CBCHMAC_1k(b *testing.B) { benchDecryptCBCHMAC(b, 32, 1032) } func BenchmarkDecryptAES256_CBCHMAC_64k(b *testing.B) { benchDecryptCBCHMAC(b, 32, 65536) } func BenchmarkDecryptAES256_CBCHMAC_1MB(b *testing.B) { benchDecryptCBCHMAC(b, 32, 1048576) } func BenchmarkDecryptAES256_CBCHMAC_64MB(b *testing.B) { benchDecryptCBCHMAC(b, 32, 67108864) } concat_kdf.go000066400000000000000000000034461313450123100345300ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto" "encoding/binary" "hash" "io" ) type concatKDF struct { z, info []byte i uint32 cache []byte hasher hash.Hash } // NewConcatKDF builds a KDF reader based on the given inputs. func NewConcatKDF(hash crypto.Hash, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo []byte) io.Reader { buffer := make([]byte, len(algID)+len(ptyUInfo)+len(ptyVInfo)+len(supPubInfo)+len(supPrivInfo)) n := 0 n += copy(buffer, algID) n += copy(buffer[n:], ptyUInfo) n += copy(buffer[n:], ptyVInfo) n += copy(buffer[n:], supPubInfo) copy(buffer[n:], supPrivInfo) hasher := hash.New() return &concatKDF{ z: z, info: buffer, hasher: hasher, cache: []byte{}, i: 1, } } func (ctx *concatKDF) Read(out []byte) (int, error) { copied := copy(out, ctx.cache) ctx.cache = ctx.cache[copied:] for copied < len(out) { ctx.hasher.Reset() // Write on a hash.Hash never fails _ = binary.Write(ctx.hasher, binary.BigEndian, ctx.i) _, _ = ctx.hasher.Write(ctx.z) _, _ = ctx.hasher.Write(ctx.info) hash := ctx.hasher.Sum(nil) chunkCopied := copy(out[copied:], hash) copied += chunkCopied ctx.cache = hash[chunkCopied:] ctx.i++ } return copied, nil } concat_kdf_test.go000066400000000000000000000073731313450123100355720ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto" "testing" ) // Taken from: https://tools.ietf.org/id/draft-ietf-jose-json-web-algorithms-38.txt func TestVectorConcatKDF(t *testing.T) { z := []byte{ 158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, 140, 254, 144, 196} algID := []byte{0, 0, 0, 7, 65, 49, 50, 56, 71, 67, 77} ptyUInfo := []byte{0, 0, 0, 5, 65, 108, 105, 99, 101} ptyVInfo := []byte{0, 0, 0, 3, 66, 111, 98} supPubInfo := []byte{0, 0, 0, 128} supPrivInfo := []byte{} expected := []byte{ 86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26} ckdf := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo) out0 := make([]byte, 9) out1 := make([]byte, 7) read0, err := ckdf.Read(out0) if err != nil { t.Error("error when reading from concat kdf reader", err) return } read1, err := ckdf.Read(out1) if err != nil { t.Error("error when reading from concat kdf reader", err) return } if read0+read1 != len(out0)+len(out1) { t.Error("did not receive enough bytes from concat kdf reader") return } out := []byte{} out = append(out, out0...) out = append(out, out1...) if bytes.Compare(out, expected) != 0 { t.Error("did not receive expected output from concat kdf reader") return } } func TestCache(t *testing.T) { z := []byte{ 158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, 140, 254, 144, 196} algID := []byte{1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4} ptyUInfo := []byte{1, 2, 3, 4} ptyVInfo := []byte{4, 3, 2, 1} supPubInfo := []byte{} supPrivInfo := []byte{} outputs := [][]byte{} // Read the same amount of data in different chunk sizes chunkSizes := []int{1, 2, 4, 8, 16, 32, 64, 128, 256, 512} for _, c := range chunkSizes { out := make([]byte, 1024) reader := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo) for i := 0; i < 1024; i += c { _, _ = reader.Read(out[i : i+c]) } outputs = append(outputs, out) } for i := range outputs { if bytes.Compare(outputs[i], outputs[(i+1)%len(outputs)]) != 0 { t.Error("not all outputs from KDF matched") } } } func benchmarkKDF(b *testing.B, total int) { z := []byte{ 158, 86, 217, 29, 129, 113, 53, 211, 114, 131, 66, 131, 191, 132, 38, 156, 251, 49, 110, 163, 218, 128, 106, 72, 246, 218, 167, 121, 140, 254, 144, 196} algID := []byte{1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4} ptyUInfo := []byte{1, 2, 3, 4} ptyVInfo := []byte{4, 3, 2, 1} supPubInfo := []byte{} supPrivInfo := []byte{} out := make([]byte, total) reader := NewConcatKDF(crypto.SHA256, z, algID, ptyUInfo, ptyVInfo, supPubInfo, supPrivInfo) b.ResetTimer() b.SetBytes(int64(total)) for i := 0; i < b.N; i++ { _, _ = reader.Read(out) } } func BenchmarkConcatKDF_1k(b *testing.B) { benchmarkKDF(b, 1024) } func BenchmarkConcatKDF_64k(b *testing.B) { benchmarkKDF(b, 65536) } func BenchmarkConcatKDF_1MB(b *testing.B) { benchmarkKDF(b, 1048576) } func BenchmarkConcatKDF_64MB(b *testing.B) { benchmarkKDF(b, 67108864) } ecdh_es.go000066400000000000000000000032021313450123100340150ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto" "crypto/ecdsa" "encoding/binary" ) // DeriveECDHES derives a shared encryption key using ECDH/ConcatKDF as described in JWE/JWA. func DeriveECDHES(alg string, apuData, apvData []byte, priv *ecdsa.PrivateKey, pub *ecdsa.PublicKey, size int) []byte { // algId, partyUInfo, partyVInfo inputs must be prefixed with the length algID := lengthPrefixed([]byte(alg)) ptyUInfo := lengthPrefixed(apuData) ptyVInfo := lengthPrefixed(apvData) // suppPubInfo is the encoded length of the output size in bits supPubInfo := make([]byte, 4) binary.BigEndian.PutUint32(supPubInfo, uint32(size)*8) z, _ := priv.PublicKey.Curve.ScalarMult(pub.X, pub.Y, priv.D.Bytes()) reader := NewConcatKDF(crypto.SHA256, z.Bytes(), algID, ptyUInfo, ptyVInfo, supPubInfo, []byte{}) key := make([]byte, size) // Read on the KDF will never fail _, _ = reader.Read(key) return key } func lengthPrefixed(data []byte) []byte { out := make([]byte, len(data)+4) binary.BigEndian.PutUint32(out, uint32(len(data))) copy(out[4:], data) return out } ecdh_es_test.go000066400000000000000000000051651313450123100350660ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "encoding/base64" "math/big" "testing" ) // Example keys from JWA, Appendix C var aliceKey = &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ Curve: elliptic.P256(), X: fromBase64Int("gI0GAILBdu7T53akrFmMyGcsF3n5dO7MmwNBHKW5SV0="), Y: fromBase64Int("SLW_xSffzlPWrHEVI30DHM_4egVwt3NQqeUD7nMFpps="), }, D: fromBase64Int("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo="), } var bobKey = &ecdsa.PrivateKey{ PublicKey: ecdsa.PublicKey{ Curve: elliptic.P256(), X: fromBase64Int("weNJy2HscCSM6AEDTDg04biOvhFhyyWvOHQfeF_PxMQ="), Y: fromBase64Int("e8lnCO-AlStT-NJVX-crhB7QRYhiix03illJOVAOyck="), }, D: fromBase64Int("VEmDZpDXXK8p8N0Cndsxs924q6nS1RXFASRl6BfUqdw="), } // Build big int from base64-encoded string. Strips whitespace (for testing). func fromBase64Int(data string) *big.Int { val, err := base64.URLEncoding.DecodeString(data) if err != nil { panic("Invalid test data") } return new(big.Int).SetBytes(val) } func TestVectorECDHES(t *testing.T) { apuData := []byte("Alice") apvData := []byte("Bob") expected := []byte{ 86, 170, 141, 234, 248, 35, 109, 32, 92, 34, 40, 205, 113, 167, 16, 26} output := DeriveECDHES("A128GCM", apuData, apvData, bobKey, &aliceKey.PublicKey, 16) if bytes.Compare(output, expected) != 0 { t.Error("output did not match what we expect, got", output, "wanted", expected) } } func BenchmarkECDHES_128(b *testing.B) { apuData := []byte("APU") apvData := []byte("APV") b.ResetTimer() for i := 0; i < b.N; i++ { DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 16) } } func BenchmarkECDHES_192(b *testing.B) { apuData := []byte("APU") apvData := []byte("APV") b.ResetTimer() for i := 0; i < b.N; i++ { DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 24) } } func BenchmarkECDHES_256(b *testing.B) { apuData := []byte("APU") apvData := []byte("APV") b.ResetTimer() for i := 0; i < b.N; i++ { DeriveECDHES("ID", apuData, apvData, bobKey, &aliceKey.PublicKey, 32) } } key_wrap.go000066400000000000000000000050561313450123100342550ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "crypto/cipher" "crypto/subtle" "encoding/binary" "errors" ) var defaultIV = []byte{0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6, 0xA6} // KeyWrap implements NIST key wrapping; it wraps a content encryption key (cek) with the given block cipher. func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) { if len(cek)%8 != 0 { return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") } n := len(cek) / 8 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], cek[i*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer, defaultIV) for t := 0; t < 6*n; t++ { copy(buffer[8:], r[t%n]) block.Encrypt(buffer, buffer) binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] = buffer[i] ^ tBytes[i] } copy(r[t%n], buffer[8:]) } out := make([]byte, (n+1)*8) copy(out, buffer[:8]) for i := range r { copy(out[(i+1)*8:], r[i]) } return out, nil } // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher. func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) { if len(ciphertext)%8 != 0 { return nil, errors.New("square/go-jose: key wrap input must be 8 byte blocks") } n := (len(ciphertext) / 8) - 1 r := make([][]byte, n) for i := range r { r[i] = make([]byte, 8) copy(r[i], ciphertext[(i+1)*8:]) } buffer := make([]byte, 16) tBytes := make([]byte, 8) copy(buffer[:8], ciphertext[:8]) for t := 6*n - 1; t >= 0; t-- { binary.BigEndian.PutUint64(tBytes, uint64(t+1)) for i := 0; i < 8; i++ { buffer[i] = buffer[i] ^ tBytes[i] } copy(buffer[8:], r[t%n]) block.Decrypt(buffer, buffer) copy(r[t%n], buffer[8:]) } if subtle.ConstantTimeCompare(buffer[:8], defaultIV) == 0 { return nil, errors.New("square/go-jose: failed to unwrap key") } out := make([]byte, n*8) for i := range r { copy(out[i*8:], r[i]) } return out, nil } key_wrap_test.go000066400000000000000000000076041313450123100353150ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/cipher/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package josecipher import ( "bytes" "crypto/aes" "encoding/hex" "testing" ) func TestAesKeyWrap(t *testing.T) { // Test vectors from: http://csrc.nist.gov/groups/ST/toolkit/documents/kms/key-wrap.pdf kek0, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F") cek0, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF") expected0, _ := hex.DecodeString("1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5") kek1, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F1011121314151617") cek1, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF") expected1, _ := hex.DecodeString("96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D") kek2, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F") cek2, _ := hex.DecodeString("00112233445566778899AABBCCDDEEFF0001020304050607") expected2, _ := hex.DecodeString("A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1") block0, _ := aes.NewCipher(kek0) block1, _ := aes.NewCipher(kek1) block2, _ := aes.NewCipher(kek2) out0, _ := KeyWrap(block0, cek0) out1, _ := KeyWrap(block1, cek1) out2, _ := KeyWrap(block2, cek2) if bytes.Compare(out0, expected0) != 0 { t.Error("output 0 not as expected, got", out0, "wanted", expected0) } if bytes.Compare(out1, expected1) != 0 { t.Error("output 1 not as expected, got", out1, "wanted", expected1) } if bytes.Compare(out2, expected2) != 0 { t.Error("output 2 not as expected, got", out2, "wanted", expected2) } unwrap0, _ := KeyUnwrap(block0, out0) unwrap1, _ := KeyUnwrap(block1, out1) unwrap2, _ := KeyUnwrap(block2, out2) if bytes.Compare(unwrap0, cek0) != 0 { t.Error("key unwrap did not return original input, got", unwrap0, "wanted", cek0) } if bytes.Compare(unwrap1, cek1) != 0 { t.Error("key unwrap did not return original input, got", unwrap1, "wanted", cek1) } if bytes.Compare(unwrap2, cek2) != 0 { t.Error("key unwrap did not return original input, got", unwrap2, "wanted", cek2) } } func TestAesKeyWrapInvalid(t *testing.T) { kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F") // Invalid unwrap input (bit flipped) input0, _ := hex.DecodeString("1EA68C1A8112B447AEF34BD8FB5A7B828D3E862371D2CFE5") block, _ := aes.NewCipher(kek) _, err := KeyUnwrap(block, input0) if err == nil { t.Error("key unwrap failed to detect invalid input") } // Invalid unwrap input (truncated) input1, _ := hex.DecodeString("1EA68C1A8112B447AEF34BD8FB5A7B828D3E862371D2CF") _, err = KeyUnwrap(block, input1) if err == nil { t.Error("key unwrap failed to detect truncated input") } // Invalid wrap input (not multiple of 8) input2, _ := hex.DecodeString("0123456789ABCD") _, err = KeyWrap(block, input2) if err == nil { t.Error("key wrap accepted invalid input") } } func BenchmarkAesKeyWrap(b *testing.B) { kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F") key, _ := hex.DecodeString("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") block, _ := aes.NewCipher(kek) b.ResetTimer() for i := 0; i < b.N; i++ { KeyWrap(block, key) } } func BenchmarkAesKeyUnwrap(b *testing.B) { kek, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F") input, _ := hex.DecodeString("1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5") block, _ := aes.NewCipher(kek) b.ResetTimer() for i := 0; i < b.N; i++ { KeyUnwrap(block, input) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/crypter.go000066400000000000000000000224061313450123100327270ustar00rootroot00000000000000/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto/ecdsa" "crypto/rsa" "fmt" "reflect" ) // Encrypter represents an encrypter which produces an encrypted JWE object. type Encrypter interface { Encrypt(plaintext []byte) (*JsonWebEncryption, error) EncryptWithAuthData(plaintext []byte, aad []byte) (*JsonWebEncryption, error) SetCompression(alg CompressionAlgorithm) } // MultiEncrypter represents an encrypter which supports multiple recipients. type MultiEncrypter interface { Encrypt(plaintext []byte) (*JsonWebEncryption, error) EncryptWithAuthData(plaintext []byte, aad []byte) (*JsonWebEncryption, error) SetCompression(alg CompressionAlgorithm) AddRecipient(alg KeyAlgorithm, encryptionKey interface{}) error } // A generic content cipher type contentCipher interface { keySize() int encrypt(cek []byte, aad, plaintext []byte) (*aeadParts, error) decrypt(cek []byte, aad []byte, parts *aeadParts) ([]byte, error) } // A key generator (for generating/getting a CEK) type keyGenerator interface { keySize() int genKey() ([]byte, rawHeader, error) } // A generic key encrypter type keyEncrypter interface { encryptKey(cek []byte, alg KeyAlgorithm) (recipientInfo, error) // Encrypt a key } // A generic key decrypter type keyDecrypter interface { decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) // Decrypt a key } // A generic encrypter based on the given key encrypter and content cipher. type genericEncrypter struct { contentAlg ContentEncryption compressionAlg CompressionAlgorithm cipher contentCipher recipients []recipientKeyInfo keyGenerator keyGenerator } type recipientKeyInfo struct { keyID string keyAlg KeyAlgorithm keyEncrypter keyEncrypter } // SetCompression sets a compression algorithm to be applied before encryption. func (ctx *genericEncrypter) SetCompression(compressionAlg CompressionAlgorithm) { ctx.compressionAlg = compressionAlg } // NewEncrypter creates an appropriate encrypter based on the key type func NewEncrypter(alg KeyAlgorithm, enc ContentEncryption, encryptionKey interface{}) (Encrypter, error) { encrypter := &genericEncrypter{ contentAlg: enc, compressionAlg: NONE, recipients: []recipientKeyInfo{}, cipher: getContentCipher(enc), } if encrypter.cipher == nil { return nil, ErrUnsupportedAlgorithm } var keyID string var rawKey interface{} switch encryptionKey := encryptionKey.(type) { case *JsonWebKey: keyID = encryptionKey.KeyID rawKey = encryptionKey.Key default: rawKey = encryptionKey } switch alg { case DIRECT: // Direct encryption mode must be treated differently if reflect.TypeOf(rawKey) != reflect.TypeOf([]byte{}) { return nil, ErrUnsupportedKeyType } encrypter.keyGenerator = staticKeyGenerator{ key: rawKey.([]byte), } recipient, _ := newSymmetricRecipient(alg, rawKey.([]byte)) if keyID != "" { recipient.keyID = keyID } encrypter.recipients = []recipientKeyInfo{recipient} return encrypter, nil case ECDH_ES: // ECDH-ES (w/o key wrapping) is similar to DIRECT mode typeOf := reflect.TypeOf(rawKey) if typeOf != reflect.TypeOf(&ecdsa.PublicKey{}) { return nil, ErrUnsupportedKeyType } encrypter.keyGenerator = ecKeyGenerator{ size: encrypter.cipher.keySize(), algID: string(enc), publicKey: rawKey.(*ecdsa.PublicKey), } recipient, _ := newECDHRecipient(alg, rawKey.(*ecdsa.PublicKey)) if keyID != "" { recipient.keyID = keyID } encrypter.recipients = []recipientKeyInfo{recipient} return encrypter, nil default: // Can just add a standard recipient encrypter.keyGenerator = randomKeyGenerator{ size: encrypter.cipher.keySize(), } err := encrypter.AddRecipient(alg, encryptionKey) return encrypter, err } } // NewMultiEncrypter creates a multi-encrypter based on the given parameters func NewMultiEncrypter(enc ContentEncryption) (MultiEncrypter, error) { cipher := getContentCipher(enc) if cipher == nil { return nil, ErrUnsupportedAlgorithm } encrypter := &genericEncrypter{ contentAlg: enc, compressionAlg: NONE, recipients: []recipientKeyInfo{}, cipher: cipher, keyGenerator: randomKeyGenerator{ size: cipher.keySize(), }, } return encrypter, nil } func (ctx *genericEncrypter) AddRecipient(alg KeyAlgorithm, encryptionKey interface{}) (err error) { var recipient recipientKeyInfo switch alg { case DIRECT, ECDH_ES: return fmt.Errorf("square/go-jose: key algorithm '%s' not supported in multi-recipient mode", alg) } recipient, err = makeJWERecipient(alg, encryptionKey) if err == nil { ctx.recipients = append(ctx.recipients, recipient) } return err } func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKeyInfo, error) { switch encryptionKey := encryptionKey.(type) { case *rsa.PublicKey: return newRSARecipient(alg, encryptionKey) case *ecdsa.PublicKey: return newECDHRecipient(alg, encryptionKey) case []byte: return newSymmetricRecipient(alg, encryptionKey) case *JsonWebKey: recipient, err := makeJWERecipient(alg, encryptionKey.Key) if err == nil && encryptionKey.KeyID != "" { recipient.keyID = encryptionKey.KeyID } return recipient, err default: return recipientKeyInfo{}, ErrUnsupportedKeyType } } // newDecrypter creates an appropriate decrypter based on the key type func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) { switch decryptionKey := decryptionKey.(type) { case *rsa.PrivateKey: return &rsaDecrypterSigner{ privateKey: decryptionKey, }, nil case *ecdsa.PrivateKey: return &ecDecrypterSigner{ privateKey: decryptionKey, }, nil case []byte: return &symmetricKeyCipher{ key: decryptionKey, }, nil case *JsonWebKey: return newDecrypter(decryptionKey.Key) default: return nil, ErrUnsupportedKeyType } } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) Encrypt(plaintext []byte) (*JsonWebEncryption, error) { return ctx.EncryptWithAuthData(plaintext, nil) } // Implementation of encrypt method producing a JWE object. func (ctx *genericEncrypter) EncryptWithAuthData(plaintext, aad []byte) (*JsonWebEncryption, error) { obj := &JsonWebEncryption{} obj.aad = aad obj.protected = &rawHeader{ Enc: ctx.contentAlg, } obj.recipients = make([]recipientInfo, len(ctx.recipients)) if len(ctx.recipients) == 0 { return nil, fmt.Errorf("square/go-jose: no recipients to encrypt to") } cek, headers, err := ctx.keyGenerator.genKey() if err != nil { return nil, err } obj.protected.merge(&headers) for i, info := range ctx.recipients { recipient, err := info.keyEncrypter.encryptKey(cek, info.keyAlg) if err != nil { return nil, err } recipient.header.Alg = string(info.keyAlg) if info.keyID != "" { recipient.header.Kid = info.keyID } obj.recipients[i] = recipient } if len(ctx.recipients) == 1 { // Move per-recipient headers into main protected header if there's // only a single recipient. obj.protected.merge(obj.recipients[0].header) obj.recipients[0].header = nil } if ctx.compressionAlg != NONE { plaintext, err = compress(ctx.compressionAlg, plaintext) if err != nil { return nil, err } obj.protected.Zip = ctx.compressionAlg } authData := obj.computeAuthData() parts, err := ctx.cipher.encrypt(cek, authData, plaintext) if err != nil { return nil, err } obj.iv = parts.iv obj.ciphertext = parts.ciphertext obj.tag = parts.tag return obj, nil } // Decrypt and validate the object and return the plaintext. func (obj JsonWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) { headers := obj.mergedHeaders(nil) if len(headers.Crit) > 0 { return nil, fmt.Errorf("square/go-jose: unsupported crit header") } decrypter, err := newDecrypter(decryptionKey) if err != nil { return nil, err } cipher := getContentCipher(headers.Enc) if cipher == nil { return nil, fmt.Errorf("square/go-jose: unsupported enc value '%s'", string(headers.Enc)) } generator := randomKeyGenerator{ size: cipher.keySize(), } parts := &aeadParts{ iv: obj.iv, ciphertext: obj.ciphertext, tag: obj.tag, } authData := obj.computeAuthData() var plaintext []byte for _, recipient := range obj.recipients { recipientHeaders := obj.mergedHeaders(&recipient) cek, err := decrypter.decryptKey(recipientHeaders, &recipient, generator) if err == nil { // Found a valid CEK -- let's try to decrypt. plaintext, err = cipher.decrypt(cek, authData, parts) if err == nil { break } } } if plaintext == nil { return nil, ErrCryptoFailure } // The "zip" header paramter may only be present in the protected header. if obj.protected.Zip != "" { plaintext, err = decompress(obj.protected.Zip, plaintext) } return plaintext, err } crypter_test.go000066400000000000000000000731401313450123100337100ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "fmt" "io" "testing" ) // We generate only a single RSA and EC key for testing, speeds up tests. var rsaTestKey, _ = rsa.GenerateKey(rand.Reader, 2048) var ecTestKey256, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) var ecTestKey384, _ = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) var ecTestKey521, _ = ecdsa.GenerateKey(elliptic.P521(), rand.Reader) func RoundtripJWE(keyAlg KeyAlgorithm, encAlg ContentEncryption, compressionAlg CompressionAlgorithm, serializer func(*JsonWebEncryption) (string, error), corrupter func(*JsonWebEncryption) bool, aad []byte, encryptionKey interface{}, decryptionKey interface{}) error { enc, err := NewEncrypter(keyAlg, encAlg, encryptionKey) if err != nil { return fmt.Errorf("error on new encrypter: %s", err) } enc.SetCompression(compressionAlg) input := []byte("Lorem ipsum dolor sit amet") obj, err := enc.EncryptWithAuthData(input, aad) if err != nil { return fmt.Errorf("error in encrypt: %s", err) } msg, err := serializer(obj) if err != nil { return fmt.Errorf("error in serializer: %s", err) } parsed, err := ParseEncrypted(msg) if err != nil { return fmt.Errorf("error in parse: %s, on msg '%s'", err, msg) } // (Maybe) mangle object skip := corrupter(parsed) if skip { return fmt.Errorf("corrupter indicated message should be skipped") } if bytes.Compare(parsed.GetAuthData(), aad) != 0 { return fmt.Errorf("auth data in parsed object does not match") } output, err := parsed.Decrypt(decryptionKey) if err != nil { return fmt.Errorf("error on decrypt: %s", err) } if bytes.Compare(input, output) != 0 { return fmt.Errorf("Decrypted output does not match input, got '%s' but wanted '%s'", output, input) } return nil } func TestRoundtripsJWE(t *testing.T) { // Test matrix keyAlgs := []KeyAlgorithm{ DIRECT, ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW, A128KW, A192KW, A256KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, A192GCMKW, A256GCMKW} encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512} zipAlgs := []CompressionAlgorithm{NONE, DEFLATE} serializers := []func(*JsonWebEncryption) (string, error){ func(obj *JsonWebEncryption) (string, error) { return obj.CompactSerialize() }, func(obj *JsonWebEncryption) (string, error) { return obj.FullSerialize(), nil }, } corrupter := func(obj *JsonWebEncryption) bool { return false } // Note: can't use AAD with compact serialization aads := [][]byte{ nil, []byte("Ut enim ad minim veniam"), } // Test all different configurations for _, alg := range keyAlgs { for _, enc := range encAlgs { for _, key := range generateTestKeys(alg, enc) { for _, zip := range zipAlgs { for i, serializer := range serializers { err := RoundtripJWE(alg, enc, zip, serializer, corrupter, aads[i], key.enc, key.dec) if err != nil { t.Error(err, alg, enc, zip, i) } } } } } } } func TestRoundtripsJWECorrupted(t *testing.T) { // Test matrix keyAlgs := []KeyAlgorithm{DIRECT, ECDH_ES, ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW} encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512} zipAlgs := []CompressionAlgorithm{NONE, DEFLATE} serializers := []func(*JsonWebEncryption) (string, error){ func(obj *JsonWebEncryption) (string, error) { return obj.CompactSerialize() }, func(obj *JsonWebEncryption) (string, error) { return obj.FullSerialize(), nil }, } bitflip := func(slice []byte) bool { if len(slice) > 0 { slice[0] ^= 0xFF return false } return true } corrupters := []func(*JsonWebEncryption) bool{ func(obj *JsonWebEncryption) bool { // Set invalid ciphertext return bitflip(obj.ciphertext) }, func(obj *JsonWebEncryption) bool { // Set invalid auth tag return bitflip(obj.tag) }, func(obj *JsonWebEncryption) bool { // Set invalid AAD return bitflip(obj.aad) }, func(obj *JsonWebEncryption) bool { // Mess with encrypted key return bitflip(obj.recipients[0].encryptedKey) }, func(obj *JsonWebEncryption) bool { // Mess with GCM-KW auth tag return bitflip(obj.protected.Tag.bytes()) }, } // Note: can't use AAD with compact serialization aads := [][]byte{ nil, []byte("Ut enim ad minim veniam"), } // Test all different configurations for _, alg := range keyAlgs { for _, enc := range encAlgs { for _, key := range generateTestKeys(alg, enc) { for _, zip := range zipAlgs { for i, serializer := range serializers { for j, corrupter := range corrupters { err := RoundtripJWE(alg, enc, zip, serializer, corrupter, aads[i], key.enc, key.dec) if err == nil { t.Error("failed to detect corrupt data", err, alg, enc, zip, i, j) } } } } } } } } func TestEncrypterWithJWKAndKeyID(t *testing.T) { enc, err := NewEncrypter(A128KW, A128GCM, &JsonWebKey{ KeyID: "test-id", Key: []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, }) if err != nil { t.Error(err) } ciphertext, _ := enc.Encrypt([]byte("Lorem ipsum dolor sit amet")) serialized1, _ := ciphertext.CompactSerialize() serialized2 := ciphertext.FullSerialize() parsed1, _ := ParseEncrypted(serialized1) parsed2, _ := ParseEncrypted(serialized2) if parsed1.Header.KeyID != "test-id" { t.Errorf("expected message to have key id from JWK, but found '%s' instead", parsed1.Header.KeyID) } if parsed2.Header.KeyID != "test-id" { t.Errorf("expected message to have key id from JWK, but found '%s' instead", parsed2.Header.KeyID) } } func TestEncrypterWithBrokenRand(t *testing.T) { keyAlgs := []KeyAlgorithm{ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW} encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512} serializer := func(obj *JsonWebEncryption) (string, error) { return obj.CompactSerialize() } corrupter := func(obj *JsonWebEncryption) bool { return false } // Break rand reader readers := []func() io.Reader{ // Totally broken func() io.Reader { return bytes.NewReader([]byte{}) }, // Not enough bytes func() io.Reader { return io.LimitReader(rand.Reader, 20) }, } defer resetRandReader() for _, alg := range keyAlgs { for _, enc := range encAlgs { for _, key := range generateTestKeys(alg, enc) { for i, getReader := range readers { randReader = getReader() err := RoundtripJWE(alg, enc, NONE, serializer, corrupter, nil, key.enc, key.dec) if err == nil { t.Error("encrypter should fail if rand is broken", i) } } } } } } func TestNewEncrypterErrors(t *testing.T) { _, err := NewEncrypter("XYZ", "XYZ", nil) if err == nil { t.Error("was able to instantiate encrypter with invalid cipher") } _, err = NewMultiEncrypter("XYZ") if err == nil { t.Error("was able to instantiate multi-encrypter with invalid cipher") } _, err = NewEncrypter(DIRECT, A128GCM, nil) if err == nil { t.Error("was able to instantiate encrypter with invalid direct key") } _, err = NewEncrypter(ECDH_ES, A128GCM, nil) if err == nil { t.Error("was able to instantiate encrypter with invalid EC key") } } func TestMultiRecipientJWE(t *testing.T) { enc, err := NewMultiEncrypter(A128GCM) if err != nil { panic(err) } err = enc.AddRecipient(RSA_OAEP, &rsaTestKey.PublicKey) if err != nil { t.Error("error when adding RSA recipient", err) } sharedKey := []byte{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, } err = enc.AddRecipient(A256GCMKW, sharedKey) if err != nil { t.Error("error when adding AES recipient: ", err) return } input := []byte("Lorem ipsum dolor sit amet") obj, err := enc.Encrypt(input) if err != nil { t.Error("error in encrypt: ", err) return } msg := obj.FullSerialize() parsed, err := ParseEncrypted(msg) if err != nil { t.Error("error in parse: ", err) return } output, err := parsed.Decrypt(rsaTestKey) if err != nil { t.Error("error on decrypt with RSA: ", err) return } if bytes.Compare(input, output) != 0 { t.Error("Decrypted output does not match input: ", output, input) return } output, err = parsed.Decrypt(sharedKey) if err != nil { t.Error("error on decrypt with AES: ", err) return } if bytes.Compare(input, output) != 0 { t.Error("Decrypted output does not match input", output, input) return } } func TestMultiRecipientErrors(t *testing.T) { enc, err := NewMultiEncrypter(A128GCM) if err != nil { panic(err) } input := []byte("Lorem ipsum dolor sit amet") _, err = enc.Encrypt(input) if err == nil { t.Error("should fail when encrypting to zero recipients") } err = enc.AddRecipient(DIRECT, nil) if err == nil { t.Error("should reject DIRECT mode when encrypting to multiple recipients") } err = enc.AddRecipient(ECDH_ES, nil) if err == nil { t.Error("should reject ECDH_ES mode when encrypting to multiple recipients") } err = enc.AddRecipient(RSA1_5, nil) if err == nil { t.Error("should reject invalid recipient key") } } type testKey struct { enc, dec interface{} } func symmetricTestKey(size int) []testKey { key, _, _ := randomKeyGenerator{size: size}.genKey() return []testKey{ testKey{ enc: key, dec: key, }, testKey{ enc: &JsonWebKey{KeyID: "test", Key: key}, dec: &JsonWebKey{KeyID: "test", Key: key}, }, } } func generateTestKeys(keyAlg KeyAlgorithm, encAlg ContentEncryption) []testKey { switch keyAlg { case DIRECT: return symmetricTestKey(getContentCipher(encAlg).keySize()) case ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW: return []testKey{ testKey{ dec: ecTestKey256, enc: &ecTestKey256.PublicKey, }, testKey{ dec: ecTestKey384, enc: &ecTestKey384.PublicKey, }, testKey{ dec: ecTestKey521, enc: &ecTestKey521.PublicKey, }, testKey{ dec: &JsonWebKey{KeyID: "test", Key: ecTestKey256}, enc: &JsonWebKey{KeyID: "test", Key: &ecTestKey256.PublicKey}, }, } case A128GCMKW, A128KW: return symmetricTestKey(16) case A192GCMKW, A192KW: return symmetricTestKey(24) case A256GCMKW, A256KW: return symmetricTestKey(32) case RSA1_5, RSA_OAEP, RSA_OAEP_256: return []testKey{testKey{ dec: rsaTestKey, enc: &rsaTestKey.PublicKey, }} } panic("Must update test case") } func RunRoundtripsJWE(b *testing.B, alg KeyAlgorithm, enc ContentEncryption, zip CompressionAlgorithm, priv, pub interface{}) { serializer := func(obj *JsonWebEncryption) (string, error) { return obj.CompactSerialize() } corrupter := func(obj *JsonWebEncryption) bool { return false } b.ResetTimer() for i := 0; i < b.N; i++ { err := RoundtripJWE(alg, enc, zip, serializer, corrupter, nil, pub, priv) if err != nil { b.Error(err) } } } var ( chunks = map[string][]byte{ "1B": make([]byte, 1), "64B": make([]byte, 64), "1KB": make([]byte, 1024), "64KB": make([]byte, 65536), "1MB": make([]byte, 1048576), "64MB": make([]byte, 67108864), } symKey, _, _ = randomKeyGenerator{size: 32}.genKey() encrypters = map[string]Encrypter{ "OAEPAndGCM": mustEncrypter(RSA_OAEP, A128GCM, &rsaTestKey.PublicKey), "PKCSAndGCM": mustEncrypter(RSA1_5, A128GCM, &rsaTestKey.PublicKey), "OAEPAndCBC": mustEncrypter(RSA_OAEP, A128CBC_HS256, &rsaTestKey.PublicKey), "PKCSAndCBC": mustEncrypter(RSA1_5, A128CBC_HS256, &rsaTestKey.PublicKey), "DirectGCM128": mustEncrypter(DIRECT, A128GCM, symKey), "DirectCBC128": mustEncrypter(DIRECT, A128CBC_HS256, symKey), "DirectGCM256": mustEncrypter(DIRECT, A256GCM, symKey), "DirectCBC256": mustEncrypter(DIRECT, A256CBC_HS512, symKey), "AESKWAndGCM128": mustEncrypter(A128KW, A128GCM, symKey), "AESKWAndCBC256": mustEncrypter(A256KW, A256GCM, symKey), "ECDHOnP256AndGCM128": mustEncrypter(ECDH_ES, A128GCM, &ecTestKey256.PublicKey), "ECDHOnP384AndGCM128": mustEncrypter(ECDH_ES, A128GCM, &ecTestKey384.PublicKey), "ECDHOnP521AndGCM128": mustEncrypter(ECDH_ES, A128GCM, &ecTestKey521.PublicKey), } ) func BenchmarkEncrypt1BWithOAEPAndGCM(b *testing.B) { benchEncrypt("1B", "OAEPAndGCM", b) } func BenchmarkEncrypt64BWithOAEPAndGCM(b *testing.B) { benchEncrypt("64B", "OAEPAndGCM", b) } func BenchmarkEncrypt1KBWithOAEPAndGCM(b *testing.B) { benchEncrypt("1KB", "OAEPAndGCM", b) } func BenchmarkEncrypt64KBWithOAEPAndGCM(b *testing.B) { benchEncrypt("64KB", "OAEPAndGCM", b) } func BenchmarkEncrypt1MBWithOAEPAndGCM(b *testing.B) { benchEncrypt("1MB", "OAEPAndGCM", b) } func BenchmarkEncrypt64MBWithOAEPAndGCM(b *testing.B) { benchEncrypt("64MB", "OAEPAndGCM", b) } func BenchmarkEncrypt1BWithPKCSAndGCM(b *testing.B) { benchEncrypt("1B", "PKCSAndGCM", b) } func BenchmarkEncrypt64BWithPKCSAndGCM(b *testing.B) { benchEncrypt("64B", "PKCSAndGCM", b) } func BenchmarkEncrypt1KBWithPKCSAndGCM(b *testing.B) { benchEncrypt("1KB", "PKCSAndGCM", b) } func BenchmarkEncrypt64KBWithPKCSAndGCM(b *testing.B) { benchEncrypt("64KB", "PKCSAndGCM", b) } func BenchmarkEncrypt1MBWithPKCSAndGCM(b *testing.B) { benchEncrypt("1MB", "PKCSAndGCM", b) } func BenchmarkEncrypt64MBWithPKCSAndGCM(b *testing.B) { benchEncrypt("64MB", "PKCSAndGCM", b) } func BenchmarkEncrypt1BWithOAEPAndCBC(b *testing.B) { benchEncrypt("1B", "OAEPAndCBC", b) } func BenchmarkEncrypt64BWithOAEPAndCBC(b *testing.B) { benchEncrypt("64B", "OAEPAndCBC", b) } func BenchmarkEncrypt1KBWithOAEPAndCBC(b *testing.B) { benchEncrypt("1KB", "OAEPAndCBC", b) } func BenchmarkEncrypt64KBWithOAEPAndCBC(b *testing.B) { benchEncrypt("64KB", "OAEPAndCBC", b) } func BenchmarkEncrypt1MBWithOAEPAndCBC(b *testing.B) { benchEncrypt("1MB", "OAEPAndCBC", b) } func BenchmarkEncrypt64MBWithOAEPAndCBC(b *testing.B) { benchEncrypt("64MB", "OAEPAndCBC", b) } func BenchmarkEncrypt1BWithPKCSAndCBC(b *testing.B) { benchEncrypt("1B", "PKCSAndCBC", b) } func BenchmarkEncrypt64BWithPKCSAndCBC(b *testing.B) { benchEncrypt("64B", "PKCSAndCBC", b) } func BenchmarkEncrypt1KBWithPKCSAndCBC(b *testing.B) { benchEncrypt("1KB", "PKCSAndCBC", b) } func BenchmarkEncrypt64KBWithPKCSAndCBC(b *testing.B) { benchEncrypt("64KB", "PKCSAndCBC", b) } func BenchmarkEncrypt1MBWithPKCSAndCBC(b *testing.B) { benchEncrypt("1MB", "PKCSAndCBC", b) } func BenchmarkEncrypt64MBWithPKCSAndCBC(b *testing.B) { benchEncrypt("64MB", "PKCSAndCBC", b) } func BenchmarkEncrypt1BWithDirectGCM128(b *testing.B) { benchEncrypt("1B", "DirectGCM128", b) } func BenchmarkEncrypt64BWithDirectGCM128(b *testing.B) { benchEncrypt("64B", "DirectGCM128", b) } func BenchmarkEncrypt1KBWithDirectGCM128(b *testing.B) { benchEncrypt("1KB", "DirectGCM128", b) } func BenchmarkEncrypt64KBWithDirectGCM128(b *testing.B) { benchEncrypt("64KB", "DirectGCM128", b) } func BenchmarkEncrypt1MBWithDirectGCM128(b *testing.B) { benchEncrypt("1MB", "DirectGCM128", b) } func BenchmarkEncrypt64MBWithDirectGCM128(b *testing.B) { benchEncrypt("64MB", "DirectGCM128", b) } func BenchmarkEncrypt1BWithDirectCBC128(b *testing.B) { benchEncrypt("1B", "DirectCBC128", b) } func BenchmarkEncrypt64BWithDirectCBC128(b *testing.B) { benchEncrypt("64B", "DirectCBC128", b) } func BenchmarkEncrypt1KBWithDirectCBC128(b *testing.B) { benchEncrypt("1KB", "DirectCBC128", b) } func BenchmarkEncrypt64KBWithDirectCBC128(b *testing.B) { benchEncrypt("64KB", "DirectCBC128", b) } func BenchmarkEncrypt1MBWithDirectCBC128(b *testing.B) { benchEncrypt("1MB", "DirectCBC128", b) } func BenchmarkEncrypt64MBWithDirectCBC128(b *testing.B) { benchEncrypt("64MB", "DirectCBC128", b) } func BenchmarkEncrypt1BWithDirectGCM256(b *testing.B) { benchEncrypt("1B", "DirectGCM256", b) } func BenchmarkEncrypt64BWithDirectGCM256(b *testing.B) { benchEncrypt("64B", "DirectGCM256", b) } func BenchmarkEncrypt1KBWithDirectGCM256(b *testing.B) { benchEncrypt("1KB", "DirectGCM256", b) } func BenchmarkEncrypt64KBWithDirectGCM256(b *testing.B) { benchEncrypt("64KB", "DirectGCM256", b) } func BenchmarkEncrypt1MBWithDirectGCM256(b *testing.B) { benchEncrypt("1MB", "DirectGCM256", b) } func BenchmarkEncrypt64MBWithDirectGCM256(b *testing.B) { benchEncrypt("64MB", "DirectGCM256", b) } func BenchmarkEncrypt1BWithDirectCBC256(b *testing.B) { benchEncrypt("1B", "DirectCBC256", b) } func BenchmarkEncrypt64BWithDirectCBC256(b *testing.B) { benchEncrypt("64B", "DirectCBC256", b) } func BenchmarkEncrypt1KBWithDirectCBC256(b *testing.B) { benchEncrypt("1KB", "DirectCBC256", b) } func BenchmarkEncrypt64KBWithDirectCBC256(b *testing.B) { benchEncrypt("64KB", "DirectCBC256", b) } func BenchmarkEncrypt1MBWithDirectCBC256(b *testing.B) { benchEncrypt("1MB", "DirectCBC256", b) } func BenchmarkEncrypt64MBWithDirectCBC256(b *testing.B) { benchEncrypt("64MB", "DirectCBC256", b) } func BenchmarkEncrypt1BWithAESKWAndGCM128(b *testing.B) { benchEncrypt("1B", "AESKWAndGCM128", b) } func BenchmarkEncrypt64BWithAESKWAndGCM128(b *testing.B) { benchEncrypt("64B", "AESKWAndGCM128", b) } func BenchmarkEncrypt1KBWithAESKWAndGCM128(b *testing.B) { benchEncrypt("1KB", "AESKWAndGCM128", b) } func BenchmarkEncrypt64KBWithAESKWAndGCM128(b *testing.B) { benchEncrypt("64KB", "AESKWAndGCM128", b) } func BenchmarkEncrypt1MBWithAESKWAndGCM128(b *testing.B) { benchEncrypt("1MB", "AESKWAndGCM128", b) } func BenchmarkEncrypt64MBWithAESKWAndGCM128(b *testing.B) { benchEncrypt("64MB", "AESKWAndGCM128", b) } func BenchmarkEncrypt1BWithAESKWAndCBC256(b *testing.B) { benchEncrypt("1B", "AESKWAndCBC256", b) } func BenchmarkEncrypt64BWithAESKWAndCBC256(b *testing.B) { benchEncrypt("64B", "AESKWAndCBC256", b) } func BenchmarkEncrypt1KBWithAESKWAndCBC256(b *testing.B) { benchEncrypt("1KB", "AESKWAndCBC256", b) } func BenchmarkEncrypt64KBWithAESKWAndCBC256(b *testing.B) { benchEncrypt("64KB", "AESKWAndCBC256", b) } func BenchmarkEncrypt1MBWithAESKWAndCBC256(b *testing.B) { benchEncrypt("1MB", "AESKWAndCBC256", b) } func BenchmarkEncrypt64MBWithAESKWAndCBC256(b *testing.B) { benchEncrypt("64MB", "AESKWAndCBC256", b) } func BenchmarkEncrypt1BWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("1B", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt64BWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("64B", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt1KBWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("1KB", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt64KBWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("64KB", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt1MBWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("1MB", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt64MBWithECDHOnP256AndGCM128(b *testing.B) { benchEncrypt("64MB", "ECDHOnP256AndGCM128", b) } func BenchmarkEncrypt1BWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("1B", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt64BWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("64B", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt1KBWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("1KB", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt64KBWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("64KB", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt1MBWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("1MB", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt64MBWithECDHOnP384AndGCM128(b *testing.B) { benchEncrypt("64MB", "ECDHOnP384AndGCM128", b) } func BenchmarkEncrypt1BWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("1B", "ECDHOnP521AndGCM128", b) } func BenchmarkEncrypt64BWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("64B", "ECDHOnP521AndGCM128", b) } func BenchmarkEncrypt1KBWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("1KB", "ECDHOnP521AndGCM128", b) } func BenchmarkEncrypt64KBWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("64KB", "ECDHOnP521AndGCM128", b) } func BenchmarkEncrypt1MBWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("1MB", "ECDHOnP521AndGCM128", b) } func BenchmarkEncrypt64MBWithECDHOnP521AndGCM128(b *testing.B) { benchEncrypt("64MB", "ECDHOnP521AndGCM128", b) } func benchEncrypt(chunkKey, primKey string, b *testing.B) { data, ok := chunks[chunkKey] if !ok { b.Fatalf("unknown chunk size %s", chunkKey) } enc, ok := encrypters[primKey] if !ok { b.Fatalf("unknown encrypter %s", primKey) } b.SetBytes(int64(len(data))) for i := 0; i < b.N; i++ { enc.Encrypt(data) } } var ( decryptionKeys = map[string]interface{}{ "OAEPAndGCM": rsaTestKey, "PKCSAndGCM": rsaTestKey, "OAEPAndCBC": rsaTestKey, "PKCSAndCBC": rsaTestKey, "DirectGCM128": symKey, "DirectCBC128": symKey, "DirectGCM256": symKey, "DirectCBC256": symKey, "AESKWAndGCM128": symKey, "AESKWAndCBC256": symKey, "ECDHOnP256AndGCM128": ecTestKey256, "ECDHOnP384AndGCM128": ecTestKey384, "ECDHOnP521AndGCM128": ecTestKey521, } ) func BenchmarkDecrypt1BWithOAEPAndGCM(b *testing.B) { benchDecrypt("1B", "OAEPAndGCM", b) } func BenchmarkDecrypt64BWithOAEPAndGCM(b *testing.B) { benchDecrypt("64B", "OAEPAndGCM", b) } func BenchmarkDecrypt1KBWithOAEPAndGCM(b *testing.B) { benchDecrypt("1KB", "OAEPAndGCM", b) } func BenchmarkDecrypt64KBWithOAEPAndGCM(b *testing.B) { benchDecrypt("64KB", "OAEPAndGCM", b) } func BenchmarkDecrypt1MBWithOAEPAndGCM(b *testing.B) { benchDecrypt("1MB", "OAEPAndGCM", b) } func BenchmarkDecrypt64MBWithOAEPAndGCM(b *testing.B) { benchDecrypt("64MB", "OAEPAndGCM", b) } func BenchmarkDecrypt1BWithPKCSAndGCM(b *testing.B) { benchDecrypt("1B", "PKCSAndGCM", b) } func BenchmarkDecrypt64BWithPKCSAndGCM(b *testing.B) { benchDecrypt("64B", "PKCSAndGCM", b) } func BenchmarkDecrypt1KBWithPKCSAndGCM(b *testing.B) { benchDecrypt("1KB", "PKCSAndGCM", b) } func BenchmarkDecrypt64KBWithPKCSAndGCM(b *testing.B) { benchDecrypt("64KB", "PKCSAndGCM", b) } func BenchmarkDecrypt1MBWithPKCSAndGCM(b *testing.B) { benchDecrypt("1MB", "PKCSAndGCM", b) } func BenchmarkDecrypt64MBWithPKCSAndGCM(b *testing.B) { benchDecrypt("64MB", "PKCSAndGCM", b) } func BenchmarkDecrypt1BWithOAEPAndCBC(b *testing.B) { benchDecrypt("1B", "OAEPAndCBC", b) } func BenchmarkDecrypt64BWithOAEPAndCBC(b *testing.B) { benchDecrypt("64B", "OAEPAndCBC", b) } func BenchmarkDecrypt1KBWithOAEPAndCBC(b *testing.B) { benchDecrypt("1KB", "OAEPAndCBC", b) } func BenchmarkDecrypt64KBWithOAEPAndCBC(b *testing.B) { benchDecrypt("64KB", "OAEPAndCBC", b) } func BenchmarkDecrypt1MBWithOAEPAndCBC(b *testing.B) { benchDecrypt("1MB", "OAEPAndCBC", b) } func BenchmarkDecrypt64MBWithOAEPAndCBC(b *testing.B) { benchDecrypt("64MB", "OAEPAndCBC", b) } func BenchmarkDecrypt1BWithPKCSAndCBC(b *testing.B) { benchDecrypt("1B", "PKCSAndCBC", b) } func BenchmarkDecrypt64BWithPKCSAndCBC(b *testing.B) { benchDecrypt("64B", "PKCSAndCBC", b) } func BenchmarkDecrypt1KBWithPKCSAndCBC(b *testing.B) { benchDecrypt("1KB", "PKCSAndCBC", b) } func BenchmarkDecrypt64KBWithPKCSAndCBC(b *testing.B) { benchDecrypt("64KB", "PKCSAndCBC", b) } func BenchmarkDecrypt1MBWithPKCSAndCBC(b *testing.B) { benchDecrypt("1MB", "PKCSAndCBC", b) } func BenchmarkDecrypt64MBWithPKCSAndCBC(b *testing.B) { benchDecrypt("64MB", "PKCSAndCBC", b) } func BenchmarkDecrypt1BWithDirectGCM128(b *testing.B) { benchDecrypt("1B", "DirectGCM128", b) } func BenchmarkDecrypt64BWithDirectGCM128(b *testing.B) { benchDecrypt("64B", "DirectGCM128", b) } func BenchmarkDecrypt1KBWithDirectGCM128(b *testing.B) { benchDecrypt("1KB", "DirectGCM128", b) } func BenchmarkDecrypt64KBWithDirectGCM128(b *testing.B) { benchDecrypt("64KB", "DirectGCM128", b) } func BenchmarkDecrypt1MBWithDirectGCM128(b *testing.B) { benchDecrypt("1MB", "DirectGCM128", b) } func BenchmarkDecrypt64MBWithDirectGCM128(b *testing.B) { benchDecrypt("64MB", "DirectGCM128", b) } func BenchmarkDecrypt1BWithDirectCBC128(b *testing.B) { benchDecrypt("1B", "DirectCBC128", b) } func BenchmarkDecrypt64BWithDirectCBC128(b *testing.B) { benchDecrypt("64B", "DirectCBC128", b) } func BenchmarkDecrypt1KBWithDirectCBC128(b *testing.B) { benchDecrypt("1KB", "DirectCBC128", b) } func BenchmarkDecrypt64KBWithDirectCBC128(b *testing.B) { benchDecrypt("64KB", "DirectCBC128", b) } func BenchmarkDecrypt1MBWithDirectCBC128(b *testing.B) { benchDecrypt("1MB", "DirectCBC128", b) } func BenchmarkDecrypt64MBWithDirectCBC128(b *testing.B) { benchDecrypt("64MB", "DirectCBC128", b) } func BenchmarkDecrypt1BWithDirectGCM256(b *testing.B) { benchDecrypt("1B", "DirectGCM256", b) } func BenchmarkDecrypt64BWithDirectGCM256(b *testing.B) { benchDecrypt("64B", "DirectGCM256", b) } func BenchmarkDecrypt1KBWithDirectGCM256(b *testing.B) { benchDecrypt("1KB", "DirectGCM256", b) } func BenchmarkDecrypt64KBWithDirectGCM256(b *testing.B) { benchDecrypt("64KB", "DirectGCM256", b) } func BenchmarkDecrypt1MBWithDirectGCM256(b *testing.B) { benchDecrypt("1MB", "DirectGCM256", b) } func BenchmarkDecrypt64MBWithDirectGCM256(b *testing.B) { benchDecrypt("64MB", "DirectGCM256", b) } func BenchmarkDecrypt1BWithDirectCBC256(b *testing.B) { benchDecrypt("1B", "DirectCBC256", b) } func BenchmarkDecrypt64BWithDirectCBC256(b *testing.B) { benchDecrypt("64B", "DirectCBC256", b) } func BenchmarkDecrypt1KBWithDirectCBC256(b *testing.B) { benchDecrypt("1KB", "DirectCBC256", b) } func BenchmarkDecrypt64KBWithDirectCBC256(b *testing.B) { benchDecrypt("64KB", "DirectCBC256", b) } func BenchmarkDecrypt1MBWithDirectCBC256(b *testing.B) { benchDecrypt("1MB", "DirectCBC256", b) } func BenchmarkDecrypt64MBWithDirectCBC256(b *testing.B) { benchDecrypt("64MB", "DirectCBC256", b) } func BenchmarkDecrypt1BWithAESKWAndGCM128(b *testing.B) { benchDecrypt("1B", "AESKWAndGCM128", b) } func BenchmarkDecrypt64BWithAESKWAndGCM128(b *testing.B) { benchDecrypt("64B", "AESKWAndGCM128", b) } func BenchmarkDecrypt1KBWithAESKWAndGCM128(b *testing.B) { benchDecrypt("1KB", "AESKWAndGCM128", b) } func BenchmarkDecrypt64KBWithAESKWAndGCM128(b *testing.B) { benchDecrypt("64KB", "AESKWAndGCM128", b) } func BenchmarkDecrypt1MBWithAESKWAndGCM128(b *testing.B) { benchDecrypt("1MB", "AESKWAndGCM128", b) } func BenchmarkDecrypt64MBWithAESKWAndGCM128(b *testing.B) { benchDecrypt("64MB", "AESKWAndGCM128", b) } func BenchmarkDecrypt1BWithAESKWAndCBC256(b *testing.B) { benchDecrypt("1B", "AESKWAndCBC256", b) } func BenchmarkDecrypt64BWithAESKWAndCBC256(b *testing.B) { benchDecrypt("64B", "AESKWAndCBC256", b) } func BenchmarkDecrypt1KBWithAESKWAndCBC256(b *testing.B) { benchDecrypt("1KB", "AESKWAndCBC256", b) } func BenchmarkDecrypt64KBWithAESKWAndCBC256(b *testing.B) { benchDecrypt("64KB", "AESKWAndCBC256", b) } func BenchmarkDecrypt1MBWithAESKWAndCBC256(b *testing.B) { benchDecrypt("1MB", "AESKWAndCBC256", b) } func BenchmarkDecrypt64MBWithAESKWAndCBC256(b *testing.B) { benchDecrypt("64MB", "AESKWAndCBC256", b) } func BenchmarkDecrypt1BWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("1B", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt64BWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("64B", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt1KBWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("1KB", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt64KBWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("64KB", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt1MBWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("1MB", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt64MBWithECDHOnP256AndGCM128(b *testing.B) { benchDecrypt("64MB", "ECDHOnP256AndGCM128", b) } func BenchmarkDecrypt1BWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("1B", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt64BWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("64B", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt1KBWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("1KB", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt64KBWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("64KB", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt1MBWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("1MB", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt64MBWithECDHOnP384AndGCM128(b *testing.B) { benchDecrypt("64MB", "ECDHOnP384AndGCM128", b) } func BenchmarkDecrypt1BWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("1B", "ECDHOnP521AndGCM128", b) } func BenchmarkDecrypt64BWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("64B", "ECDHOnP521AndGCM128", b) } func BenchmarkDecrypt1KBWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("1KB", "ECDHOnP521AndGCM128", b) } func BenchmarkDecrypt64KBWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("64KB", "ECDHOnP521AndGCM128", b) } func BenchmarkDecrypt1MBWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("1MB", "ECDHOnP521AndGCM128", b) } func BenchmarkDecrypt64MBWithECDHOnP521AndGCM128(b *testing.B) { benchDecrypt("64MB", "ECDHOnP521AndGCM128", b) } func benchDecrypt(chunkKey, primKey string, b *testing.B) { chunk, ok := chunks[chunkKey] if !ok { b.Fatalf("unknown chunk size %s", chunkKey) } enc, ok := encrypters[primKey] if !ok { b.Fatalf("unknown encrypter %s", primKey) } dec, ok := decryptionKeys[primKey] if !ok { b.Fatalf("unknown decryption key %s", primKey) } data, err := enc.Encrypt(chunk) if err != nil { b.Fatal(err) } b.SetBytes(int64(len(chunk))) b.ResetTimer() for i := 0; i < b.N; i++ { data.Decrypt(dec) } } func mustEncrypter(keyAlg KeyAlgorithm, encAlg ContentEncryption, encryptionKey interface{}) Encrypter { enc, err := NewEncrypter(keyAlg, encAlg, encryptionKey) if err != nil { panic(err) } return enc } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/doc.go000066400000000000000000000017721313450123100320070ustar00rootroot00000000000000/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* Package jose aims to provide an implementation of the Javascript Object Signing and Encryption set of standards. For the moment, it mainly focuses on encryption and signing based on the JSON Web Encryption and JSON Web Signature standards. The library supports both the compact and full serialization formats, and has optional support for multiple recipients. */ package jose // import "gopkg.in/square/go-jose.v1" docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/doc_test.go000066400000000000000000000152041313450123100330410ustar00rootroot00000000000000/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "crypto/ecdsa" "crypto/rand" "crypto/rsa" "fmt" ) // Dummy encrypter for use in examples var encrypter, _ = NewEncrypter(DIRECT, A128GCM, []byte{}) func Example_jWE() { // Generate a public/private key pair to use for this example. The library // also provides two utility functions (LoadPublicKey and LoadPrivateKey) // that can be used to load keys from PEM/DER-encoded data. privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } // Instantiate an encrypter using RSA-OAEP with AES128-GCM. An error would // indicate that the selected algorithm(s) are not currently supported. publicKey := &privateKey.PublicKey encrypter, err := NewEncrypter(RSA_OAEP, A128GCM, publicKey) if err != nil { panic(err) } // Encrypt a sample plaintext. Calling the encrypter returns an encrypted // JWE object, which can then be serialized for output afterwards. An error // would indicate a problem in an underlying cryptographic primitive. var plaintext = []byte("Lorem ipsum dolor sit amet") object, err := encrypter.Encrypt(plaintext) if err != nil { panic(err) } // Serialize the encrypted object using the full serialization format. // Alternatively you can also use the compact format here by calling // object.CompactSerialize() instead. serialized := object.FullSerialize() // Parse the serialized, encrypted JWE object. An error would indicate that // the given input did not represent a valid message. object, err = ParseEncrypted(serialized) if err != nil { panic(err) } // Now we can decrypt and get back our original plaintext. An error here // would indicate the the message failed to decrypt, e.g. because the auth // tag was broken or the message was tampered with. decrypted, err := object.Decrypt(privateKey) if err != nil { panic(err) } fmt.Printf(string(decrypted)) // output: Lorem ipsum dolor sit amet } func Example_jWS() { // Generate a public/private key pair to use for this example. The library // also provides two utility functions (LoadPublicKey and LoadPrivateKey) // that can be used to load keys from PEM/DER-encoded data. privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { panic(err) } // Instantiate a signer using RSASSA-PSS (SHA512) with the given private key. signer, err := NewSigner(PS512, privateKey) if err != nil { panic(err) } // Sign a sample payload. Calling the signer returns a protected JWS object, // which can then be serialized for output afterwards. An error would // indicate a problem in an underlying cryptographic primitive. var payload = []byte("Lorem ipsum dolor sit amet") object, err := signer.Sign(payload) if err != nil { panic(err) } // Serialize the encrypted object using the full serialization format. // Alternatively you can also use the compact format here by calling // object.CompactSerialize() instead. serialized := object.FullSerialize() // Parse the serialized, protected JWS object. An error would indicate that // the given input did not represent a valid message. object, err = ParseSigned(serialized) if err != nil { panic(err) } // Now we can verify the signature on the payload. An error here would // indicate the the message failed to verify, e.g. because the signature was // broken or the message was tampered with. output, err := object.Verify(&privateKey.PublicKey) if err != nil { panic(err) } fmt.Printf(string(output)) // output: Lorem ipsum dolor sit amet } func ExampleNewEncrypter_publicKey() { var publicKey *rsa.PublicKey // Instantiate an encrypter using RSA-OAEP with AES128-GCM. NewEncrypter(RSA_OAEP, A128GCM, publicKey) // Instantiate an encrypter using RSA-PKCS1v1.5 with AES128-CBC+HMAC. NewEncrypter(RSA1_5, A128CBC_HS256, publicKey) } func ExampleNewEncrypter_symmetric() { var sharedKey []byte // Instantiate an encrypter using AES128-GCM with AES-GCM key wrap. NewEncrypter(A128GCMKW, A128GCM, sharedKey) // Instantiate an encrypter using AES256-GCM directly, w/o key wrapping. NewEncrypter(DIRECT, A256GCM, sharedKey) } func ExampleNewSigner_publicKey() { var rsaPrivateKey *rsa.PrivateKey var ecdsaPrivateKey *ecdsa.PrivateKey // Instantiate a signer using RSA-PKCS#1v1.5 with SHA-256. NewSigner(RS256, rsaPrivateKey) // Instantiate a signer using ECDSA with SHA-384. NewSigner(ES384, ecdsaPrivateKey) } func ExampleNewSigner_symmetric() { var sharedKey []byte // Instantiate an signer using HMAC-SHA256. NewSigner(HS256, sharedKey) // Instantiate an signer using HMAC-SHA512. NewSigner(HS512, sharedKey) } func ExampleNewMultiEncrypter() { var publicKey *rsa.PublicKey var sharedKey []byte // Instantiate an encrypter using AES-GCM. encrypter, err := NewMultiEncrypter(A128GCM) if err != nil { panic(err) } // Add a recipient using a shared key with AES-GCM key wap err = encrypter.AddRecipient(A128GCMKW, sharedKey) if err != nil { panic(err) } // Add a recipient using an RSA public key with RSA-OAEP err = encrypter.AddRecipient(RSA_OAEP, publicKey) if err != nil { panic(err) } } func ExampleNewMultiSigner() { var privateKey *rsa.PrivateKey var sharedKey []byte // Instantiate a signer for multiple recipients. signer := NewMultiSigner() // Add a recipient using a shared key with HMAC-SHA256 err := signer.AddRecipient(HS256, sharedKey) if err != nil { panic(err) } // Add a recipient using an RSA private key with RSASSA-PSS with SHA384 err = signer.AddRecipient(PS384, privateKey) if err != nil { panic(err) } } func ExampleEncrypter_encrypt() { // Encrypt a plaintext in order to get an encrypted JWE object. var plaintext = []byte("This is a secret message") encrypter.Encrypt(plaintext) } func ExampleEncrypter_encryptWithAuthData() { // Encrypt a plaintext in order to get an encrypted JWE object. Also attach // some additional authenticated data (AAD) to the object. Note that objects // with attached AAD can only be represented using full serialization. var plaintext = []byte("This is a secret message") var aad = []byte("This is authenticated, but public data") encrypter.EncryptWithAuthData(plaintext, aad) } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/encoding.go000066400000000000000000000114731313450123100330270ustar00rootroot00000000000000/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "compress/flate" "encoding/base64" "encoding/binary" "io" "math/big" "regexp" "strings" ) var stripWhitespaceRegex = regexp.MustCompile("\\s") // Url-safe base64 encode that strips padding func base64URLEncode(data []byte) string { var result = base64.URLEncoding.EncodeToString(data) return strings.TrimRight(result, "=") } // Url-safe base64 decoder that adds padding func base64URLDecode(data string) ([]byte, error) { var missing = (4 - len(data)%4) % 4 data += strings.Repeat("=", missing) return base64.URLEncoding.DecodeString(data) } // Helper function to serialize known-good objects. // Precondition: value is not a nil pointer. func mustSerializeJSON(value interface{}) []byte { out, err := MarshalJSON(value) if err != nil { panic(err) } // We never want to serialize the top-level value "null," since it's not a // valid JOSE message. But if a caller passes in a nil pointer to this method, // MarshalJSON will happily serialize it as the top-level value "null". If // that value is then embedded in another operation, for instance by being // base64-encoded and fed as input to a signing algorithm // (https://github.com/square/go-jose/issues/22), the result will be // incorrect. Because this method is intended for known-good objects, and a nil // pointer is not a known-good object, we are free to panic in this case. // Note: It's not possible to directly check whether the data pointed at by an // interface is a nil pointer, so we do this hacky workaround. // https://groups.google.com/forum/#!topic/golang-nuts/wnH302gBa4I if string(out) == "null" { panic("Tried to serialize a nil pointer.") } return out } // Strip all newlines and whitespace func stripWhitespace(data string) string { return stripWhitespaceRegex.ReplaceAllString(data, "") } // Perform compression based on algorithm func compress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return deflate(input) default: return nil, ErrUnsupportedAlgorithm } } // Perform decompression based on algorithm func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) { switch algorithm { case DEFLATE: return inflate(input) default: return nil, ErrUnsupportedAlgorithm } } // Compress with DEFLATE func deflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) // Writing to byte buffer, err is always nil writer, _ := flate.NewWriter(output, 1) _, _ = io.Copy(writer, bytes.NewBuffer(input)) err := writer.Close() return output.Bytes(), err } // Decompress with DEFLATE func inflate(input []byte) ([]byte, error) { output := new(bytes.Buffer) reader := flate.NewReader(bytes.NewBuffer(input)) _, err := io.Copy(output, reader) if err != nil { return nil, err } err = reader.Close() return output.Bytes(), err } // byteBuffer represents a slice of bytes that can be serialized to url-safe base64. type byteBuffer struct { data []byte } func newBuffer(data []byte) *byteBuffer { if data == nil { return nil } return &byteBuffer{ data: data, } } func newFixedSizeBuffer(data []byte, length int) *byteBuffer { if len(data) > length { panic("square/go-jose: invalid call to newFixedSizeBuffer (len(data) > length)") } pad := make([]byte, length-len(data)) return newBuffer(append(pad, data...)) } func newBufferFromInt(num uint64) *byteBuffer { data := make([]byte, 8) binary.BigEndian.PutUint64(data, num) return newBuffer(bytes.TrimLeft(data, "\x00")) } func (b *byteBuffer) MarshalJSON() ([]byte, error) { return MarshalJSON(b.base64()) } func (b *byteBuffer) UnmarshalJSON(data []byte) error { var encoded string err := UnmarshalJSON(data, &encoded) if err != nil { return err } if encoded == "" { return nil } decoded, err := base64URLDecode(encoded) if err != nil { return err } *b = *newBuffer(decoded) return nil } func (b *byteBuffer) base64() string { return base64URLEncode(b.data) } func (b *byteBuffer) bytes() []byte { // Handling nil here allows us to transparently handle nil slices when serializing. if b == nil { return nil } return b.data } func (b byteBuffer) bigInt() *big.Int { return new(big.Int).SetBytes(b.data) } func (b byteBuffer) toInt() int { return int(b.bigInt().Int64()) } encoding_test.go000066400000000000000000000103421313450123100340010ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/*- * Copyright 2014 Square Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jose import ( "bytes" "strings" "testing" ) func TestBase64URLEncode(t *testing.T) { // Test arrays with various sizes if base64URLEncode([]byte{}) != "" { t.Error("failed to encode empty array") } if base64URLEncode([]byte{0}) != "AA" { t.Error("failed to encode [0x00]") } if base64URLEncode([]byte{0, 1}) != "AAE" { t.Error("failed to encode [0x00, 0x01]") } if base64URLEncode([]byte{0, 1, 2}) != "AAEC" { t.Error("failed to encode [0x00, 0x01, 0x02]") } if base64URLEncode([]byte{0, 1, 2, 3}) != "AAECAw" { t.Error("failed to encode [0x00, 0x01, 0x02, 0x03]") } } func TestBase64URLDecode(t *testing.T) { // Test arrays with various sizes val, err := base64URLDecode("") if err != nil || !bytes.Equal(val, []byte{}) { t.Error("failed to decode empty array") } val, err = base64URLDecode("AA") if err != nil || !bytes.Equal(val, []byte{0}) { t.Error("failed to decode [0x00]") } val, err = base64URLDecode("AAE") if err != nil || !bytes.Equal(val, []byte{0, 1}) { t.Error("failed to decode [0x00, 0x01]") } val, err = base64URLDecode("AAEC") if err != nil || !bytes.Equal(val, []byte{0, 1, 2}) { t.Error("failed to decode [0x00, 0x01, 0x02]") } val, err = base64URLDecode("AAECAw") if err != nil || !bytes.Equal(val, []byte{0, 1, 2, 3}) { t.Error("failed to decode [0x00, 0x01, 0x02, 0x03]") } } func TestDeflateRoundtrip(t *testing.T) { original := []byte("Lorem ipsum dolor sit amet") compressed, err := deflate(original) if err != nil { panic(err) } output, err := inflate(compressed) if err != nil { panic(err) } if bytes.Compare(output, original) != 0 { t.Error("Input and output do not match") } } func TestInvalidCompression(t *testing.T) { _, err := compress("XYZ", []byte{}) if err == nil { t.Error("should not accept invalid algorithm") } _, err = decompress("XYZ", []byte{}) if err == nil { t.Error("should not accept invalid algorithm") } _, err = decompress(DEFLATE, []byte{1, 2, 3, 4}) if err == nil { t.Error("should not accept invalid data") } } func TestByteBufferTrim(t *testing.T) { buf := newBufferFromInt(1) if !bytes.Equal(buf.data, []byte{1}) { t.Error("Byte buffer for integer '1' should contain [0x01]") } buf = newBufferFromInt(65537) if !bytes.Equal(buf.data, []byte{1, 0, 1}) { t.Error("Byte buffer for integer '65537' should contain [0x01, 0x00, 0x01]") } } func TestFixedSizeBuffer(t *testing.T) { data0 := []byte{} data1 := []byte{1} data2 := []byte{1, 2} data3 := []byte{1, 2, 3} data4 := []byte{1, 2, 3, 4} buf0 := newFixedSizeBuffer(data0, 4) buf1 := newFixedSizeBuffer(data1, 4) buf2 := newFixedSizeBuffer(data2, 4) buf3 := newFixedSizeBuffer(data3, 4) buf4 := newFixedSizeBuffer(data4, 4) if !bytes.Equal(buf0.data, []byte{0, 0, 0, 0}) { t.Error("Invalid padded buffer for buf0") } if !bytes.Equal(buf1.data, []byte{0, 0, 0, 1}) { t.Error("Invalid padded buffer for buf1") } if !bytes.Equal(buf2.data, []byte{0, 0, 1, 2}) { t.Error("Invalid padded buffer for buf2") } if !bytes.Equal(buf3.data, []byte{0, 1, 2, 3}) { t.Error("Invalid padded buffer for buf3") } if !bytes.Equal(buf4.data, []byte{1, 2, 3, 4}) { t.Error("Invalid padded buffer for buf4") } } func TestSerializeJSONRejectsNil(t *testing.T) { defer func() { r := recover() if r == nil || !strings.Contains(r.(string), "nil pointer") { t.Error("serialize function should not accept nil pointer") } }() mustSerializeJSON(nil) } func TestFixedSizeBufferTooLarge(t *testing.T) { defer func() { r := recover() if r == nil { t.Error("should not be able to create fixed size buffer with oversized data") } }() newFixedSizeBuffer(make([]byte, 2), 1) } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json/000077500000000000000000000000001313450123100316555ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json/LICENSE000066400000000000000000000027071313450123100326700ustar00rootroot00000000000000Copyright (c) 2012 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json/README.md000066400000000000000000000011701313450123100331330ustar00rootroot00000000000000# Safe JSON This repository contains a fork of the `encoding/json` package from Go 1.6. The following changes were made: * Object deserialization uses case-sensitive member name matching instead of [case-insensitive matching](https://www.ietf.org/mail-archive/web/json/current/msg03763.html). This is to avoid differences in the interpretation of JOSE messages between go-jose and libraries written in other languages. * When deserializing a JSON object, we check for duplicate keys and reject the input whenever we detect a duplicate. Rather than trying to work with malformed data, we prefer to reject it right away. bench_test.go000066400000000000000000000110261313450123100342430ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json// Copyright 2011 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Large data benchmark. // The JSON data is a summary of agl's changes in the // go, webkit, and chromium open source projects. // We benchmark converting between the JSON form // and in-memory data structures. package json import ( "bytes" "compress/gzip" "io/ioutil" "os" "strings" "testing" ) type codeResponse struct { Tree *codeNode `json:"tree"` Username string `json:"username"` } type codeNode struct { Name string `json:"name"` Kids []*codeNode `json:"kids"` CLWeight float64 `json:"cl_weight"` Touches int `json:"touches"` MinT int64 `json:"min_t"` MaxT int64 `json:"max_t"` MeanT int64 `json:"mean_t"` } var codeJSON []byte var codeStruct codeResponse func codeInit() { f, err := os.Open("testdata/code.json.gz") if err != nil { panic(err) } defer f.Close() gz, err := gzip.NewReader(f) if err != nil { panic(err) } data, err := ioutil.ReadAll(gz) if err != nil { panic(err) } codeJSON = data if err := Unmarshal(codeJSON, &codeStruct); err != nil { panic("unmarshal code.json: " + err.Error()) } if data, err = Marshal(&codeStruct); err != nil { panic("marshal code.json: " + err.Error()) } if !bytes.Equal(data, codeJSON) { println("different lengths", len(data), len(codeJSON)) for i := 0; i < len(data) && i < len(codeJSON); i++ { if data[i] != codeJSON[i] { println("re-marshal: changed at byte", i) println("orig: ", string(codeJSON[i-10:i+10])) println("new: ", string(data[i-10:i+10])) break } } panic("re-marshal code.json: different result") } } func BenchmarkCodeEncoder(b *testing.B) { if codeJSON == nil { b.StopTimer() codeInit() b.StartTimer() } enc := NewEncoder(ioutil.Discard) for i := 0; i < b.N; i++ { if err := enc.Encode(&codeStruct); err != nil { b.Fatal("Encode:", err) } } b.SetBytes(int64(len(codeJSON))) } func BenchmarkCodeMarshal(b *testing.B) { if codeJSON == nil { b.StopTimer() codeInit() b.StartTimer() } for i := 0; i < b.N; i++ { if _, err := Marshal(&codeStruct); err != nil { b.Fatal("Marshal:", err) } } b.SetBytes(int64(len(codeJSON))) } func BenchmarkCodeDecoder(b *testing.B) { if codeJSON == nil { b.StopTimer() codeInit() b.StartTimer() } var buf bytes.Buffer dec := NewDecoder(&buf) var r codeResponse for i := 0; i < b.N; i++ { buf.Write(codeJSON) // hide EOF buf.WriteByte('\n') buf.WriteByte('\n') buf.WriteByte('\n') if err := dec.Decode(&r); err != nil { b.Fatal("Decode:", err) } } b.SetBytes(int64(len(codeJSON))) } func BenchmarkDecoderStream(b *testing.B) { b.StopTimer() var buf bytes.Buffer dec := NewDecoder(&buf) buf.WriteString(`"` + strings.Repeat("x", 1000000) + `"` + "\n\n\n") var x interface{} if err := dec.Decode(&x); err != nil { b.Fatal("Decode:", err) } ones := strings.Repeat(" 1\n", 300000) + "\n\n\n" b.StartTimer() for i := 0; i < b.N; i++ { if i%300000 == 0 { buf.WriteString(ones) } x = nil if err := dec.Decode(&x); err != nil || x != 1.0 { b.Fatalf("Decode: %v after %d", err, i) } } } func BenchmarkCodeUnmarshal(b *testing.B) { if codeJSON == nil { b.StopTimer() codeInit() b.StartTimer() } for i := 0; i < b.N; i++ { var r codeResponse if err := Unmarshal(codeJSON, &r); err != nil { b.Fatal("Unmmarshal:", err) } } b.SetBytes(int64(len(codeJSON))) } func BenchmarkCodeUnmarshalReuse(b *testing.B) { if codeJSON == nil { b.StopTimer() codeInit() b.StartTimer() } var r codeResponse for i := 0; i < b.N; i++ { if err := Unmarshal(codeJSON, &r); err != nil { b.Fatal("Unmmarshal:", err) } } } func BenchmarkUnmarshalString(b *testing.B) { data := []byte(`"hello, world"`) var s string for i := 0; i < b.N; i++ { if err := Unmarshal(data, &s); err != nil { b.Fatal("Unmarshal:", err) } } } func BenchmarkUnmarshalFloat64(b *testing.B) { var f float64 data := []byte(`3.14`) for i := 0; i < b.N; i++ { if err := Unmarshal(data, &f); err != nil { b.Fatal("Unmarshal:", err) } } } func BenchmarkUnmarshalInt64(b *testing.B) { var x int64 data := []byte(`3`) for i := 0; i < b.N; i++ { if err := Unmarshal(data, &x); err != nil { b.Fatal("Unmarshal:", err) } } } func BenchmarkIssue10335(b *testing.B) { b.ReportAllocs() var s struct{} j := []byte(`{"a":{ }}`) for n := 0; n < b.N; n++ { if err := Unmarshal(j, &s); err != nil { b.Fatal(err) } } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json/decode.go000066400000000000000000000712141313450123100334340ustar00rootroot00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Represents JSON data structure using native Go types: booleans, floats, // strings, arrays, and maps. package json import ( "bytes" "encoding" "encoding/base64" "errors" "fmt" "reflect" "runtime" "strconv" "unicode" "unicode/utf16" "unicode/utf8" ) // Unmarshal parses the JSON-encoded data and stores the result // in the value pointed to by v. // // Unmarshal uses the inverse of the encodings that // Marshal uses, allocating maps, slices, and pointers as necessary, // with the following additional rules: // // To unmarshal JSON into a pointer, Unmarshal first handles the case of // the JSON being the JSON literal null. In that case, Unmarshal sets // the pointer to nil. Otherwise, Unmarshal unmarshals the JSON into // the value pointed at by the pointer. If the pointer is nil, Unmarshal // allocates a new value for it to point to. // // To unmarshal JSON into a struct, Unmarshal matches incoming object // keys to the keys used by Marshal (either the struct field name or its tag), // preferring an exact match but also accepting a case-insensitive match. // Unmarshal will only set exported fields of the struct. // // To unmarshal JSON into an interface value, // Unmarshal stores one of these in the interface value: // // bool, for JSON booleans // float64, for JSON numbers // string, for JSON strings // []interface{}, for JSON arrays // map[string]interface{}, for JSON objects // nil for JSON null // // To unmarshal a JSON array into a slice, Unmarshal resets the slice length // to zero and then appends each element to the slice. // As a special case, to unmarshal an empty JSON array into a slice, // Unmarshal replaces the slice with a new empty slice. // // To unmarshal a JSON array into a Go array, Unmarshal decodes // JSON array elements into corresponding Go array elements. // If the Go array is smaller than the JSON array, // the additional JSON array elements are discarded. // If the JSON array is smaller than the Go array, // the additional Go array elements are set to zero values. // // To unmarshal a JSON object into a string-keyed map, Unmarshal first // establishes a map to use, If the map is nil, Unmarshal allocates a new map. // Otherwise Unmarshal reuses the existing map, keeping existing entries. // Unmarshal then stores key-value pairs from the JSON object into the map. // // If a JSON value is not appropriate for a given target type, // or if a JSON number overflows the target type, Unmarshal // skips that field and completes the unmarshaling as best it can. // If no more serious errors are encountered, Unmarshal returns // an UnmarshalTypeError describing the earliest such error. // // The JSON null value unmarshals into an interface, map, pointer, or slice // by setting that Go value to nil. Because null is often used in JSON to mean // ``not present,'' unmarshaling a JSON null into any other Go type has no effect // on the value and produces no error. // // When unmarshaling quoted strings, invalid UTF-8 or // invalid UTF-16 surrogate pairs are not treated as an error. // Instead, they are replaced by the Unicode replacement // character U+FFFD. // func Unmarshal(data []byte, v interface{}) error { // Check for well-formedness. // Avoids filling out half a data structure // before discovering a JSON syntax error. var d decodeState err := checkValid(data, &d.scan) if err != nil { return err } d.init(data) return d.unmarshal(v) } // Unmarshaler is the interface implemented by objects // that can unmarshal a JSON description of themselves. // The input can be assumed to be a valid encoding of // a JSON value. UnmarshalJSON must copy the JSON data // if it wishes to retain the data after returning. type Unmarshaler interface { UnmarshalJSON([]byte) error } // An UnmarshalTypeError describes a JSON value that was // not appropriate for a value of a specific Go type. type UnmarshalTypeError struct { Value string // description of JSON value - "bool", "array", "number -5" Type reflect.Type // type of Go value it could not be assigned to Offset int64 // error occurred after reading Offset bytes } func (e *UnmarshalTypeError) Error() string { return "json: cannot unmarshal " + e.Value + " into Go value of type " + e.Type.String() } // An UnmarshalFieldError describes a JSON object key that // led to an unexported (and therefore unwritable) struct field. // (No longer used; kept for compatibility.) type UnmarshalFieldError struct { Key string Type reflect.Type Field reflect.StructField } func (e *UnmarshalFieldError) Error() string { return "json: cannot unmarshal object key " + strconv.Quote(e.Key) + " into unexported field " + e.Field.Name + " of type " + e.Type.String() } // An InvalidUnmarshalError describes an invalid argument passed to Unmarshal. // (The argument to Unmarshal must be a non-nil pointer.) type InvalidUnmarshalError struct { Type reflect.Type } func (e *InvalidUnmarshalError) Error() string { if e.Type == nil { return "json: Unmarshal(nil)" } if e.Type.Kind() != reflect.Ptr { return "json: Unmarshal(non-pointer " + e.Type.String() + ")" } return "json: Unmarshal(nil " + e.Type.String() + ")" } func (d *decodeState) unmarshal(v interface{}) (err error) { defer func() { if r := recover(); r != nil { if _, ok := r.(runtime.Error); ok { panic(r) } err = r.(error) } }() rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr || rv.IsNil() { return &InvalidUnmarshalError{reflect.TypeOf(v)} } d.scan.reset() // We decode rv not rv.Elem because the Unmarshaler interface // test must be applied at the top level of the value. d.value(rv) return d.savedError } // A Number represents a JSON number literal. type Number string // String returns the literal text of the number. func (n Number) String() string { return string(n) } // Float64 returns the number as a float64. func (n Number) Float64() (float64, error) { return strconv.ParseFloat(string(n), 64) } // Int64 returns the number as an int64. func (n Number) Int64() (int64, error) { return strconv.ParseInt(string(n), 10, 64) } // isValidNumber reports whether s is a valid JSON number literal. func isValidNumber(s string) bool { // This function implements the JSON numbers grammar. // See https://tools.ietf.org/html/rfc7159#section-6 // and http://json.org/number.gif if s == "" { return false } // Optional - if s[0] == '-' { s = s[1:] if s == "" { return false } } // Digits switch { default: return false case s[0] == '0': s = s[1:] case '1' <= s[0] && s[0] <= '9': s = s[1:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // . followed by 1 or more digits. if len(s) >= 2 && s[0] == '.' && '0' <= s[1] && s[1] <= '9' { s = s[2:] for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // e or E followed by an optional - or + and // 1 or more digits. if len(s) >= 2 && (s[0] == 'e' || s[0] == 'E') { s = s[1:] if s[0] == '+' || s[0] == '-' { s = s[1:] if s == "" { return false } } for len(s) > 0 && '0' <= s[0] && s[0] <= '9' { s = s[1:] } } // Make sure we are at the end. return s == "" } // decodeState represents the state while decoding a JSON value. type decodeState struct { data []byte off int // read offset in data scan scanner nextscan scanner // for calls to nextValue savedError error useNumber bool } // errPhase is used for errors that should not happen unless // there is a bug in the JSON decoder or something is editing // the data slice while the decoder executes. var errPhase = errors.New("JSON decoder out of sync - data changing underfoot?") func (d *decodeState) init(data []byte) *decodeState { d.data = data d.off = 0 d.savedError = nil return d } // error aborts the decoding by panicking with err. func (d *decodeState) error(err error) { panic(err) } // saveError saves the first err it is called with, // for reporting at the end of the unmarshal. func (d *decodeState) saveError(err error) { if d.savedError == nil { d.savedError = err } } // next cuts off and returns the next full JSON value in d.data[d.off:]. // The next value is known to be an object or array, not a literal. func (d *decodeState) next() []byte { c := d.data[d.off] item, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // Our scanner has seen the opening brace/bracket // and thinks we're still in the middle of the object. // invent a closing brace/bracket to get it out. if c == '{' { d.scan.step(&d.scan, '}') } else { d.scan.step(&d.scan, ']') } return item } // scanWhile processes bytes in d.data[d.off:] until it // receives a scan code not equal to op. // It updates d.off and returns the new scan code. func (d *decodeState) scanWhile(op int) int { var newOp int for { if d.off >= len(d.data) { newOp = d.scan.eof() d.off = len(d.data) + 1 // mark processed EOF with len+1 } else { c := d.data[d.off] d.off++ newOp = d.scan.step(&d.scan, c) } if newOp != op { break } } return newOp } // value decodes a JSON value from d.data[d.off:] into the value. // it updates d.off to point past the decoded value. func (d *decodeState) value(v reflect.Value) { if !v.IsValid() { _, rest, err := nextValue(d.data[d.off:], &d.nextscan) if err != nil { d.error(err) } d.off = len(d.data) - len(rest) // d.scan thinks we're still at the beginning of the item. // Feed in an empty string - the shortest, simplest value - // so that it knows we got to the end of the value. if d.scan.redo { // rewind. d.scan.redo = false d.scan.step = stateBeginValue } d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') n := len(d.scan.parseState) if n > 0 && d.scan.parseState[n-1] == parseObjectKey { // d.scan thinks we just read an object key; finish the object d.scan.step(&d.scan, ':') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '"') d.scan.step(&d.scan, '}') } return } switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(v) case scanBeginObject: d.object(v) case scanBeginLiteral: d.literal(v) } } type unquotedValue struct{} // valueQuoted is like value but decodes a // quoted string literal or literal null into an interface value. // If it finds anything other than a quoted string literal or null, // valueQuoted returns unquotedValue{}. func (d *decodeState) valueQuoted() interface{} { switch op := d.scanWhile(scanSkipSpace); op { default: d.error(errPhase) case scanBeginArray: d.array(reflect.Value{}) case scanBeginObject: d.object(reflect.Value{}) case scanBeginLiteral: switch v := d.literalInterface().(type) { case nil, string: return v } } return unquotedValue{} } // indirect walks down v allocating pointers as needed, // until it gets to a non-pointer. // if it encounters an Unmarshaler, indirect stops and returns that. // if decodingNull is true, indirect stops at the last pointer so it can be set to nil. func (d *decodeState) indirect(v reflect.Value, decodingNull bool) (Unmarshaler, encoding.TextUnmarshaler, reflect.Value) { // If v is a named type and is addressable, // start with its address, so that if the type has pointer methods, // we find them. if v.Kind() != reflect.Ptr && v.Type().Name() != "" && v.CanAddr() { v = v.Addr() } for { // Load value from interface, but only if the result will be // usefully addressable. if v.Kind() == reflect.Interface && !v.IsNil() { e := v.Elem() if e.Kind() == reflect.Ptr && !e.IsNil() && (!decodingNull || e.Elem().Kind() == reflect.Ptr) { v = e continue } } if v.Kind() != reflect.Ptr { break } if v.Elem().Kind() != reflect.Ptr && decodingNull && v.CanSet() { break } if v.IsNil() { v.Set(reflect.New(v.Type().Elem())) } if v.Type().NumMethod() > 0 { if u, ok := v.Interface().(Unmarshaler); ok { return u, nil, reflect.Value{} } if u, ok := v.Interface().(encoding.TextUnmarshaler); ok { return nil, u, reflect.Value{} } } v = v.Elem() } return nil, nil, v } // array consumes an array from d.data[d.off-1:], decoding into the value v. // the first byte of the array ('[') has been read already. func (d *decodeState) array(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return } v = pv // Check type of target. switch v.Kind() { case reflect.Interface: if v.NumMethod() == 0 { // Decoding into nil interface? Switch to non-reflect code. v.Set(reflect.ValueOf(d.arrayInterface())) return } // Otherwise it's invalid. fallthrough default: d.saveError(&UnmarshalTypeError{"array", v.Type(), int64(d.off)}) d.off-- d.next() return case reflect.Array: case reflect.Slice: break } i := 0 for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) // Get element of array, growing if necessary. if v.Kind() == reflect.Slice { // Grow slice if necessary if i >= v.Cap() { newcap := v.Cap() + v.Cap()/2 if newcap < 4 { newcap = 4 } newv := reflect.MakeSlice(v.Type(), v.Len(), newcap) reflect.Copy(newv, v) v.Set(newv) } if i >= v.Len() { v.SetLen(i + 1) } } if i < v.Len() { // Decode into element. d.value(v.Index(i)) } else { // Ran out of fixed array: skip. d.value(reflect.Value{}) } i++ // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } if i < v.Len() { if v.Kind() == reflect.Array { // Array. Zero the rest. z := reflect.Zero(v.Type().Elem()) for ; i < v.Len(); i++ { v.Index(i).Set(z) } } else { v.SetLen(i) } } if i == 0 && v.Kind() == reflect.Slice { v.Set(reflect.MakeSlice(v.Type(), 0, 0)) } } var nullLiteral = []byte("null") // object consumes an object from d.data[d.off-1:], decoding into the value v. // the first byte ('{') of the object has been read already. func (d *decodeState) object(v reflect.Value) { // Check for unmarshaler. u, ut, pv := d.indirect(v, false) if u != nil { d.off-- err := u.UnmarshalJSON(d.next()) if err != nil { d.error(err) } return } if ut != nil { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } v = pv // Decoding into nil interface? Switch to non-reflect code. if v.Kind() == reflect.Interface && v.NumMethod() == 0 { v.Set(reflect.ValueOf(d.objectInterface())) return } // Check type of target: struct or map[string]T switch v.Kind() { case reflect.Map: // map must have string kind t := v.Type() if t.Key().Kind() != reflect.String { d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } if v.IsNil() { v.Set(reflect.MakeMap(t)) } case reflect.Struct: default: d.saveError(&UnmarshalTypeError{"object", v.Type(), int64(d.off)}) d.off-- d.next() // skip over { } in input return } var mapElem reflect.Value keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Figure out field corresponding to key. var subv reflect.Value destring := false // whether the value is wrapped in a string to be decoded first if v.Kind() == reflect.Map { elemType := v.Type().Elem() if !mapElem.IsValid() { mapElem = reflect.New(elemType).Elem() } else { mapElem.Set(reflect.Zero(elemType)) } subv = mapElem } else { var f *field fields := cachedTypeFields(v.Type()) for i := range fields { ff := &fields[i] if bytes.Equal(ff.nameBytes, []byte(key)) { f = ff break } } if f != nil { subv = v destring = f.quoted for _, i := range f.index { if subv.Kind() == reflect.Ptr { if subv.IsNil() { subv.Set(reflect.New(subv.Type().Elem())) } subv = subv.Elem() } subv = subv.Field(i) } } } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. if destring { switch qv := d.valueQuoted().(type) { case nil: d.literalStore(nullLiteral, subv, false) case string: d.literalStore([]byte(qv), subv, true) default: d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal unquoted value into %v", subv.Type())) } } else { d.value(subv) } // Write value back to map; // if using struct, subv points into struct already. if v.Kind() == reflect.Map { kv := reflect.ValueOf(key).Convert(v.Type().Key()) v.SetMapIndex(kv, subv) } // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } } // literal consumes a literal from d.data[d.off-1:], decoding into the value v. // The first byte of the literal has been read already // (that's how the caller knows it's a literal). func (d *decodeState) literal(v reflect.Value) { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) d.literalStore(d.data[start:d.off], v, false) } // convertNumber converts the number literal s to a float64 or a Number // depending on the setting of d.useNumber. func (d *decodeState) convertNumber(s string) (interface{}, error) { if d.useNumber { return Number(s), nil } f, err := strconv.ParseFloat(s, 64) if err != nil { return nil, &UnmarshalTypeError{"number " + s, reflect.TypeOf(0.0), int64(d.off)} } return f, nil } var numberType = reflect.TypeOf(Number("")) // literalStore decodes a literal stored in item into v. // // fromQuoted indicates whether this literal came from unwrapping a // string from the ",string" struct tag option. this is used only to // produce more helpful error messages. func (d *decodeState) literalStore(item []byte, v reflect.Value, fromQuoted bool) { // Check for unmarshaler. if len(item) == 0 { //Empty string given d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) return } wantptr := item[0] == 'n' // null u, ut, pv := d.indirect(v, wantptr) if u != nil { err := u.UnmarshalJSON(item) if err != nil { d.error(err) } return } if ut != nil { if item[0] != '"' { if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } return } s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } err := ut.UnmarshalText(s) if err != nil { d.error(err) } return } v = pv switch c := item[0]; c { case 'n': // null switch v.Kind() { case reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: v.Set(reflect.Zero(v.Type())) // otherwise, ignore null for primitives/string } case 't', 'f': // true, false value := c == 't' switch v.Kind() { default: if fromQuoted { d.saveError(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } case reflect.Bool: v.SetBool(value) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(value)) } else { d.saveError(&UnmarshalTypeError{"bool", v.Type(), int64(d.off)}) } } case '"': // string s, ok := unquoteBytes(item) if !ok { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } switch v.Kind() { default: d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) case reflect.Slice: if v.Type().Elem().Kind() != reflect.Uint8 { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) break } b := make([]byte, base64.StdEncoding.DecodedLen(len(s))) n, err := base64.StdEncoding.Decode(b, s) if err != nil { d.saveError(err) break } v.SetBytes(b[:n]) case reflect.String: v.SetString(string(s)) case reflect.Interface: if v.NumMethod() == 0 { v.Set(reflect.ValueOf(string(s))) } else { d.saveError(&UnmarshalTypeError{"string", v.Type(), int64(d.off)}) } } default: // number if c != '-' && (c < '0' || c > '9') { if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(errPhase) } } s := string(item) switch v.Kind() { default: if v.Kind() == reflect.String && v.Type() == numberType { v.SetString(s) if !isValidNumber(s) { d.error(fmt.Errorf("json: invalid number literal, trying to unmarshal %q into Number", item)) } break } if fromQuoted { d.error(fmt.Errorf("json: invalid use of ,string struct tag, trying to unmarshal %q into %v", item, v.Type())) } else { d.error(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) } case reflect.Interface: n, err := d.convertNumber(s) if err != nil { d.saveError(err) break } if v.NumMethod() != 0 { d.saveError(&UnmarshalTypeError{"number", v.Type(), int64(d.off)}) break } v.Set(reflect.ValueOf(n)) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(s, 10, 64) if err != nil || v.OverflowInt(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: n, err := strconv.ParseUint(s, 10, 64) if err != nil || v.OverflowUint(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(s, v.Type().Bits()) if err != nil || v.OverflowFloat(n) { d.saveError(&UnmarshalTypeError{"number " + s, v.Type(), int64(d.off)}) break } v.SetFloat(n) } } } // The xxxInterface routines build up a value to be stored // in an empty interface. They are not strictly necessary, // but they avoid the weight of reflection in this common case. // valueInterface is like value but returns interface{} func (d *decodeState) valueInterface() interface{} { switch d.scanWhile(scanSkipSpace) { default: d.error(errPhase) panic("unreachable") case scanBeginArray: return d.arrayInterface() case scanBeginObject: return d.objectInterface() case scanBeginLiteral: return d.literalInterface() } } // arrayInterface is like array but returns []interface{}. func (d *decodeState) arrayInterface() []interface{} { var v = make([]interface{}, 0) for { // Look ahead for ] - can only happen on first iteration. op := d.scanWhile(scanSkipSpace) if op == scanEndArray { break } // Back up so d.value can have the byte we just read. d.off-- d.scan.undo(op) v = append(v, d.valueInterface()) // Next token must be , or ]. op = d.scanWhile(scanSkipSpace) if op == scanEndArray { break } if op != scanArrayValue { d.error(errPhase) } } return v } // objectInterface is like object but returns map[string]interface{}. func (d *decodeState) objectInterface() map[string]interface{} { m := make(map[string]interface{}) keys := map[string]bool{} for { // Read opening " of string key or closing }. op := d.scanWhile(scanSkipSpace) if op == scanEndObject { // closing } - can only happen on first iteration. break } if op != scanBeginLiteral { d.error(errPhase) } // Read string key. start := d.off - 1 op = d.scanWhile(scanContinue) item := d.data[start : d.off-1] key, ok := unquote(item) if !ok { d.error(errPhase) } // Check for duplicate keys. _, ok = keys[key] if !ok { keys[key] = true } else { d.error(fmt.Errorf("json: duplicate key '%s' in object", key)) } // Read : before value. if op == scanSkipSpace { op = d.scanWhile(scanSkipSpace) } if op != scanObjectKey { d.error(errPhase) } // Read value. m[key] = d.valueInterface() // Next token must be , or }. op = d.scanWhile(scanSkipSpace) if op == scanEndObject { break } if op != scanObjectValue { d.error(errPhase) } } return m } // literalInterface is like literal but returns an interface value. func (d *decodeState) literalInterface() interface{} { // All bytes inside literal return scanContinue op code. start := d.off - 1 op := d.scanWhile(scanContinue) // Scan read one byte too far; back up. d.off-- d.scan.undo(op) item := d.data[start:d.off] switch c := item[0]; c { case 'n': // null return nil case 't', 'f': // true, false return c == 't' case '"': // string s, ok := unquote(item) if !ok { d.error(errPhase) } return s default: // number if c != '-' && (c < '0' || c > '9') { d.error(errPhase) } n, err := d.convertNumber(string(item)) if err != nil { d.saveError(err) } return n } } // getu4 decodes \uXXXX from the beginning of s, returning the hex value, // or it returns -1. func getu4(s []byte) rune { if len(s) < 6 || s[0] != '\\' || s[1] != 'u' { return -1 } r, err := strconv.ParseUint(string(s[2:6]), 16, 64) if err != nil { return -1 } return rune(r) } // unquote converts a quoted JSON string literal s into an actual string t. // The rules are different than for Go, so cannot use strconv.Unquote. func unquote(s []byte) (t string, ok bool) { s, ok = unquoteBytes(s) t = string(s) return } func unquoteBytes(s []byte) (t []byte, ok bool) { if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { return } s = s[1 : len(s)-1] // Check for unusual characters. If there are none, // then no unquoting is needed, so return a slice of the // original bytes. r := 0 for r < len(s) { c := s[r] if c == '\\' || c == '"' || c < ' ' { break } if c < utf8.RuneSelf { r++ continue } rr, size := utf8.DecodeRune(s[r:]) if rr == utf8.RuneError && size == 1 { break } r += size } if r == len(s) { return s, true } b := make([]byte, len(s)+2*utf8.UTFMax) w := copy(b, s[0:r]) for r < len(s) { // Out of room? Can only happen if s is full of // malformed UTF-8 and we're replacing each // byte with RuneError. if w >= len(b)-2*utf8.UTFMax { nb := make([]byte, (len(b)+utf8.UTFMax)*2) copy(nb, b[0:w]) b = nb } switch c := s[r]; { case c == '\\': r++ if r >= len(s) { return } switch s[r] { default: return case '"', '\\', '/', '\'': b[w] = s[r] r++ w++ case 'b': b[w] = '\b' r++ w++ case 'f': b[w] = '\f' r++ w++ case 'n': b[w] = '\n' r++ w++ case 'r': b[w] = '\r' r++ w++ case 't': b[w] = '\t' r++ w++ case 'u': r-- rr := getu4(s[r:]) if rr < 0 { return } r += 6 if utf16.IsSurrogate(rr) { rr1 := getu4(s[r:]) if dec := utf16.DecodeRune(rr, rr1); dec != unicode.ReplacementChar { // A valid pair; consume. r += 6 w += utf8.EncodeRune(b[w:], dec) break } // Invalid surrogate; fall back to replacement rune. rr = unicode.ReplacementChar } w += utf8.EncodeRune(b[w:], rr) } // Quote, control characters are invalid. case c == '"', c < ' ': return // ASCII case c < utf8.RuneSelf: b[w] = c r++ w++ // Coerce to well-formed UTF-8. default: rr, size := utf8.DecodeRune(s[r:]) r += size w += utf8.EncodeRune(b[w:], rr) } } return b[0:w], true } decode_test.go000066400000000000000000001020001313450123100344000ustar00rootroot00000000000000docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package json import ( "bytes" "encoding" "fmt" "image" "net" "reflect" "strings" "testing" "time" ) type T struct { X string Y int Z int `json:"-"` } type U struct { Alphabet string `json:"alpha"` } type V struct { F1 interface{} F2 int32 F3 Number } // ifaceNumAsFloat64/ifaceNumAsNumber are used to test unmarshaling with and // without UseNumber var ifaceNumAsFloat64 = map[string]interface{}{ "k1": float64(1), "k2": "s", "k3": []interface{}{float64(1), float64(2.0), float64(3e-3)}, "k4": map[string]interface{}{"kk1": "s", "kk2": float64(2)}, } var ifaceNumAsNumber = map[string]interface{}{ "k1": Number("1"), "k2": "s", "k3": []interface{}{Number("1"), Number("2.0"), Number("3e-3")}, "k4": map[string]interface{}{"kk1": "s", "kk2": Number("2")}, } type tx struct { x int } // A type that can unmarshal itself. type unmarshaler struct { T bool } func (u *unmarshaler) UnmarshalJSON(b []byte) error { *u = unmarshaler{true} // All we need to see that UnmarshalJSON is called. return nil } type ustruct struct { M unmarshaler } type unmarshalerText struct { T bool } // needed for re-marshaling tests func (u *unmarshalerText) MarshalText() ([]byte, error) { return []byte(""), nil } func (u *unmarshalerText) UnmarshalText(b []byte) error { *u = unmarshalerText{true} // All we need to see that UnmarshalText is called. return nil } var _ encoding.TextUnmarshaler = (*unmarshalerText)(nil) type ustructText struct { M unmarshalerText } var ( um0, um1 unmarshaler // target2 of unmarshaling ump = &um1 umtrue = unmarshaler{true} umslice = []unmarshaler{{true}} umslicep = new([]unmarshaler) umstruct = ustruct{unmarshaler{true}} um0T, um1T unmarshalerText // target2 of unmarshaling umpT = &um1T umtrueT = unmarshalerText{true} umsliceT = []unmarshalerText{{true}} umslicepT = new([]unmarshalerText) umstructT = ustructText{unmarshalerText{true}} ) // Test data structures for anonymous fields. type Point struct { Z int } type Top struct { Level0 int Embed0 *Embed0a *Embed0b `json:"e,omitempty"` // treated as named Embed0c `json:"-"` // ignored Loop Embed0p // has Point with X, Y, used Embed0q // has Point with Z, used embed // contains exported field } type Embed0 struct { Level1a int // overridden by Embed0a's Level1a with json tag Level1b int // used because Embed0a's Level1b is renamed Level1c int // used because Embed0a's Level1c is ignored Level1d int // annihilated by Embed0a's Level1d Level1e int `json:"x"` // annihilated by Embed0a.Level1e } type Embed0a struct { Level1a int `json:"Level1a,omitempty"` Level1b int `json:"LEVEL1B,omitempty"` Level1c int `json:"-"` Level1d int // annihilated by Embed0's Level1d Level1f int `json:"x"` // annihilated by Embed0's Level1e } type Embed0b Embed0 type Embed0c Embed0 type Embed0p struct { image.Point } type Embed0q struct { Point } type embed struct { Q int } type Loop struct { Loop1 int `json:",omitempty"` Loop2 int `json:",omitempty"` *Loop } // From reflect test: // The X in S6 and S7 annihilate, but they also block the X in S8.S9. type S5 struct { S6 S7 S8 } type S6 struct { X int } type S7 S6 type S8 struct { S9 } type S9 struct { X int Y int } // From reflect test: // The X in S11.S6 and S12.S6 annihilate, but they also block the X in S13.S8.S9. type S10 struct { S11 S12 S13 } type S11 struct { S6 } type S12 struct { S6 } type S13 struct { S8 } type unmarshalTest struct { in string ptr interface{} out interface{} err error useNumber bool } type XYZ struct { X interface{} Y interface{} Z interface{} } func sliceAddr(x []int) *[]int { return &x } func mapAddr(x map[string]int) *map[string]int { return &x } var unmarshalTests = []unmarshalTest{ // basic types {in: `true`, ptr: new(bool), out: true}, {in: `1`, ptr: new(int), out: 1}, {in: `1.2`, ptr: new(float64), out: 1.2}, {in: `-5`, ptr: new(int16), out: int16(-5)}, {in: `2`, ptr: new(Number), out: Number("2"), useNumber: true}, {in: `2`, ptr: new(Number), out: Number("2")}, {in: `2`, ptr: new(interface{}), out: float64(2.0)}, {in: `2`, ptr: new(interface{}), out: Number("2"), useNumber: true}, {in: `"a\u1234"`, ptr: new(string), out: "a\u1234"}, {in: `"http:\/\/"`, ptr: new(string), out: "http://"}, {in: `"g-clef: \uD834\uDD1E"`, ptr: new(string), out: "g-clef: \U0001D11E"}, {in: `"invalid: \uD834x\uDD1E"`, ptr: new(string), out: "invalid: \uFFFDx\uFFFD"}, {in: "null", ptr: new(interface{}), out: nil}, {in: `{"X": [1,2,3], "Y": 4}`, ptr: new(T), out: T{Y: 4}, err: &UnmarshalTypeError{"array", reflect.TypeOf(""), 7}}, {in: `{"x": 1}`, ptr: new(tx), out: tx{}}, {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: float64(1), F2: int32(2), F3: Number("3")}}, {in: `{"F1":1,"F2":2,"F3":3}`, ptr: new(V), out: V{F1: Number("1"), F2: int32(2), F3: Number("3")}, useNumber: true}, {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64}, {in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsNumber, useNumber: true}, // raw values with whitespace {in: "\n true ", ptr: new(bool), out: true}, {in: "\t 1 ", ptr: new(int), out: 1}, {in: "\r 1.2 ", ptr: new(float64), out: 1.2}, {in: "\t -5 \n", ptr: new(int16), out: int16(-5)}, {in: "\t \"a\\u1234\" \n", ptr: new(string), out: "a\u1234"}, // Z has a "-" tag. {in: `{"Y": 1, "Z": 2}`, ptr: new(T), out: T{Y: 1}}, {in: `{"alpha": "abc", "alphabet": "xyz"}`, ptr: new(U), out: U{Alphabet: "abc"}}, {in: `{"alpha": "abc"}`, ptr: new(U), out: U{Alphabet: "abc"}}, {in: `{"alphabet": "xyz"}`, ptr: new(U), out: U{}}, // syntax errors {in: `{"X": "foo", "Y"}`, err: &SyntaxError{"invalid character '}' after object key", 17}}, {in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", 9}}, {in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", 8}, useNumber: true}, // raw value errors {in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, {in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 5}}, {in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, {in: " false \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 8}}, {in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, {in: " 3.4 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 6}}, {in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, {in: " \"string\" \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 11}}, // array tests {in: `[1, 2, 3]`, ptr: new([3]int), out: [3]int{1, 2, 3}}, {in: `[1, 2, 3]`, ptr: new([1]int), out: [1]int{1}}, {in: `[1, 2, 3]`, ptr: new([5]int), out: [5]int{1, 2, 3, 0, 0}}, // empty array to interface test {in: `[]`, ptr: new([]interface{}), out: []interface{}{}}, {in: `null`, ptr: new([]interface{}), out: []interface{}(nil)}, {in: `{"T":[]}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": []interface{}{}}}, {in: `{"T":null}`, ptr: new(map[string]interface{}), out: map[string]interface{}{"T": interface{}(nil)}}, // composite tests {in: allValueIndent, ptr: new(All), out: allValue}, {in: allValueCompact, ptr: new(All), out: allValue}, {in: allValueIndent, ptr: new(*All), out: &allValue}, {in: allValueCompact, ptr: new(*All), out: &allValue}, {in: pallValueIndent, ptr: new(All), out: pallValue}, {in: pallValueCompact, ptr: new(All), out: pallValue}, {in: pallValueIndent, ptr: new(*All), out: &pallValue}, {in: pallValueCompact, ptr: new(*All), out: &pallValue}, // unmarshal interface test {in: `{"T":false}`, ptr: &um0, out: umtrue}, // use "false" so test will fail if custom unmarshaler is not called {in: `{"T":false}`, ptr: &ump, out: &umtrue}, {in: `[{"T":false}]`, ptr: &umslice, out: umslice}, {in: `[{"T":false}]`, ptr: &umslicep, out: &umslice}, {in: `{"M":{"T":false}}`, ptr: &umstruct, out: umstruct}, // UnmarshalText interface test {in: `"X"`, ptr: &um0T, out: umtrueT}, // use "false" so test will fail if custom unmarshaler is not called {in: `"X"`, ptr: &umpT, out: &umtrueT}, {in: `["X"]`, ptr: &umsliceT, out: umsliceT}, {in: `["X"]`, ptr: &umslicepT, out: &umsliceT}, {in: `{"M":"X"}`, ptr: &umstructT, out: umstructT}, // Overwriting of data. // This is different from package xml, but it's what we've always done. // Now documented and tested. {in: `[2]`, ptr: sliceAddr([]int{1}), out: []int{2}}, {in: `{"key": 2}`, ptr: mapAddr(map[string]int{"old": 0, "key": 1}), out: map[string]int{"key": 2}}, { in: `{ "Level0": 1, "Level1b": 2, "Level1c": 3, "x": 4, "Level1a": 5, "LEVEL1B": 6, "e": { "Level1a": 8, "Level1b": 9, "Level1c": 10, "Level1d": 11, "x": 12 }, "Loop1": 13, "Loop2": 14, "X": 15, "Y": 16, "Z": 17, "Q": 18 }`, ptr: new(Top), out: Top{ Level0: 1, Embed0: Embed0{ Level1b: 2, Level1c: 3, }, Embed0a: &Embed0a{ Level1a: 5, Level1b: 6, }, Embed0b: &Embed0b{ Level1a: 8, Level1b: 9, Level1c: 10, Level1d: 11, Level1e: 12, }, Loop: Loop{ Loop1: 13, Loop2: 14, }, Embed0p: Embed0p{ Point: image.Point{X: 15, Y: 16}, }, Embed0q: Embed0q{ Point: Point{Z: 17}, }, embed: embed{ Q: 18, }, }, }, { in: `{"X": 1,"Y":2}`, ptr: new(S5), out: S5{S8: S8{S9: S9{Y: 2}}}, }, { in: `{"X": 1,"Y":2}`, ptr: new(S10), out: S10{S13: S13{S8: S8{S9: S9{Y: 2}}}}, }, // invalid UTF-8 is coerced to valid UTF-8. { in: "\"hello\xffworld\"", ptr: new(string), out: "hello\ufffdworld", }, { in: "\"hello\xc2\xc2world\"", ptr: new(string), out: "hello\ufffd\ufffdworld", }, { in: "\"hello\xc2\xffworld\"", ptr: new(string), out: "hello\ufffd\ufffdworld", }, { in: "\"hello\\ud800world\"", ptr: new(string), out: "hello\ufffdworld", }, { in: "\"hello\\ud800\\ud800world\"", ptr: new(string), out: "hello\ufffd\ufffdworld", }, { in: "\"hello\\ud800\\ud800world\"", ptr: new(string), out: "hello\ufffd\ufffdworld", }, { in: "\"hello\xed\xa0\x80\xed\xb0\x80world\"", ptr: new(string), out: "hello\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdworld", }, // issue 8305 { in: `{"2009-11-10T23:00:00Z": "hello world"}`, ptr: &map[time.Time]string{}, err: &UnmarshalTypeError{"object", reflect.TypeOf(map[time.Time]string{}), 1}, }, } func TestMarshal(t *testing.T) { b, err := Marshal(allValue) if err != nil { t.Fatalf("Marshal allValue: %v", err) } if string(b) != allValueCompact { t.Errorf("Marshal allValueCompact") diff(t, b, []byte(allValueCompact)) return } b, err = Marshal(pallValue) if err != nil { t.Fatalf("Marshal pallValue: %v", err) } if string(b) != pallValueCompact { t.Errorf("Marshal pallValueCompact") diff(t, b, []byte(pallValueCompact)) return } } var badUTF8 = []struct { in, out string }{ {"hello\xffworld", `"hello\ufffdworld"`}, {"", `""`}, {"\xff", `"\ufffd"`}, {"\xff\xff", `"\ufffd\ufffd"`}, {"a\xffb", `"a\ufffdb"`}, {"\xe6\x97\xa5\xe6\x9c\xac\xff\xaa\x9e", `"日本\ufffd\ufffd\ufffd"`}, } func TestMarshalBadUTF8(t *testing.T) { for _, tt := range badUTF8 { b, err := Marshal(tt.in) if string(b) != tt.out || err != nil { t.Errorf("Marshal(%q) = %#q, %v, want %#q, nil", tt.in, b, err, tt.out) } } } func TestMarshalNumberZeroVal(t *testing.T) { var n Number out, err := Marshal(n) if err != nil { t.Fatal(err) } outStr := string(out) if outStr != "0" { t.Fatalf("Invalid zero val for Number: %q", outStr) } } func TestMarshalEmbeds(t *testing.T) { top := &Top{ Level0: 1, Embed0: Embed0{ Level1b: 2, Level1c: 3, }, Embed0a: &Embed0a{ Level1a: 5, Level1b: 6, }, Embed0b: &Embed0b{ Level1a: 8, Level1b: 9, Level1c: 10, Level1d: 11, Level1e: 12, }, Loop: Loop{ Loop1: 13, Loop2: 14, }, Embed0p: Embed0p{ Point: image.Point{X: 15, Y: 16}, }, Embed0q: Embed0q{ Point: Point{Z: 17}, }, embed: embed{ Q: 18, }, } b, err := Marshal(top) if err != nil { t.Fatal(err) } want := "{\"Level0\":1,\"Level1b\":2,\"Level1c\":3,\"Level1a\":5,\"LEVEL1B\":6,\"e\":{\"Level1a\":8,\"Level1b\":9,\"Level1c\":10,\"Level1d\":11,\"x\":12},\"Loop1\":13,\"Loop2\":14,\"X\":15,\"Y\":16,\"Z\":17,\"Q\":18}" if string(b) != want { t.Errorf("Wrong marshal result.\n got: %q\nwant: %q", b, want) } } func TestUnmarshal(t *testing.T) { for i, tt := range unmarshalTests { var scan scanner in := []byte(tt.in) if err := checkValid(in, &scan); err != nil { if !reflect.DeepEqual(err, tt.err) { t.Errorf("#%d: checkValid: %#v", i, err) continue } } if tt.ptr == nil { continue } // v = new(right-type) v := reflect.New(reflect.TypeOf(tt.ptr).Elem()) dec := NewDecoder(bytes.NewReader(in)) if tt.useNumber { dec.UseNumber() } if err := dec.Decode(v.Interface()); !reflect.DeepEqual(err, tt.err) { t.Errorf("#%d: %v, want %v", i, err, tt.err) continue } else if err != nil { continue } if !reflect.DeepEqual(v.Elem().Interface(), tt.out) { t.Errorf("#%d: mismatch\nhave: %#+v\nwant: %#+v", i, v.Elem().Interface(), tt.out) data, _ := Marshal(v.Elem().Interface()) println(string(data)) data, _ = Marshal(tt.out) println(string(data)) continue } // Check round trip. if tt.err == nil { enc, err := Marshal(v.Interface()) if err != nil { t.Errorf("#%d: error re-marshaling: %v", i, err) continue } vv := reflect.New(reflect.TypeOf(tt.ptr).Elem()) dec = NewDecoder(bytes.NewReader(enc)) if tt.useNumber { dec.UseNumber() } if err := dec.Decode(vv.Interface()); err != nil { t.Errorf("#%d: error re-unmarshaling %#q: %v", i, enc, err) continue } if !reflect.DeepEqual(v.Elem().Interface(), vv.Elem().Interface()) { t.Errorf("#%d: mismatch\nhave: %#+v\nwant: %#+v", i, v.Elem().Interface(), vv.Elem().Interface()) t.Errorf(" In: %q", strings.Map(noSpace, string(in))) t.Errorf("Marshal: %q", strings.Map(noSpace, string(enc))) continue } } } } func TestUnmarshalMarshal(t *testing.T) { initBig() var v interface{} if err := Unmarshal(jsonBig, &v); err != nil { t.Fatalf("Unmarshal: %v", err) } b, err := Marshal(v) if err != nil { t.Fatalf("Marshal: %v", err) } if !bytes.Equal(jsonBig, b) { t.Errorf("Marshal jsonBig") diff(t, b, jsonBig) return } } var numberTests = []struct { in string i int64 intErr string f float64 floatErr string }{ {in: "-1.23e1", intErr: "strconv.ParseInt: parsing \"-1.23e1\": invalid syntax", f: -1.23e1}, {in: "-12", i: -12, f: -12.0}, {in: "1e1000", intErr: "strconv.ParseInt: parsing \"1e1000\": invalid syntax", floatErr: "strconv.ParseFloat: parsing \"1e1000\": value out of range"}, } // Independent of Decode, basic coverage of the accessors in Number func TestNumberAccessors(t *testing.T) { for _, tt := range numberTests { n := Number(tt.in) if s := n.String(); s != tt.in { t.Errorf("Number(%q).String() is %q", tt.in, s) } if i, err := n.Int64(); err == nil && tt.intErr == "" && i != tt.i { t.Errorf("Number(%q).Int64() is %d", tt.in, i) } else if (err == nil && tt.intErr != "") || (err != nil && err.Error() != tt.intErr) { t.Errorf("Number(%q).Int64() wanted error %q but got: %v", tt.in, tt.intErr, err) } if f, err := n.Float64(); err == nil && tt.floatErr == "" && f != tt.f { t.Errorf("Number(%q).Float64() is %g", tt.in, f) } else if (err == nil && tt.floatErr != "") || (err != nil && err.Error() != tt.floatErr) { t.Errorf("Number(%q).Float64() wanted error %q but got: %v", tt.in, tt.floatErr, err) } } } func TestLargeByteSlice(t *testing.T) { s0 := make([]byte, 2000) for i := range s0 { s0[i] = byte(i) } b, err := Marshal(s0) if err != nil { t.Fatalf("Marshal: %v", err) } var s1 []byte if err := Unmarshal(b, &s1); err != nil { t.Fatalf("Unmarshal: %v", err) } if !bytes.Equal(s0, s1) { t.Errorf("Marshal large byte slice") diff(t, s0, s1) } } type Xint struct { X int } func TestUnmarshalInterface(t *testing.T) { var xint Xint var i interface{} = &xint if err := Unmarshal([]byte(`{"X":1}`), &i); err != nil { t.Fatalf("Unmarshal: %v", err) } if xint.X != 1 { t.Fatalf("Did not write to xint") } } func TestUnmarshalPtrPtr(t *testing.T) { var xint Xint pxint := &xint if err := Unmarshal([]byte(`{"X":1}`), &pxint); err != nil { t.Fatalf("Unmarshal: %v", err) } if xint.X != 1 { t.Fatalf("Did not write to xint") } } func TestEscape(t *testing.T) { const input = `"foobar"` + " [\u2028 \u2029]" const expected = `"\"foobar\"\u003chtml\u003e [\u2028 \u2029]"` b, err := Marshal(input) if err != nil { t.Fatalf("Marshal error: %v", err) } if s := string(b); s != expected { t.Errorf("Encoding of [%s]:\n got [%s]\nwant [%s]", input, s, expected) } } // WrongString is a struct that's misusing the ,string modifier. type WrongString struct { Message string `json:"result,string"` } type wrongStringTest struct { in, err string } var wrongStringTests = []wrongStringTest{ {`{"result":"x"}`, `json: invalid use of ,string struct tag, trying to unmarshal "x" into string`}, {`{"result":"foo"}`, `json: invalid use of ,string struct tag, trying to unmarshal "foo" into string`}, {`{"result":"123"}`, `json: invalid use of ,string struct tag, trying to unmarshal "123" into string`}, {`{"result":123}`, `json: invalid use of ,string struct tag, trying to unmarshal unquoted value into string`}, } // If people misuse the ,string modifier, the error message should be // helpful, telling the user that they're doing it wrong. func TestErrorMessageFromMisusedString(t *testing.T) { for n, tt := range wrongStringTests { r := strings.NewReader(tt.in) var s WrongString err := NewDecoder(r).Decode(&s) got := fmt.Sprintf("%v", err) if got != tt.err { t.Errorf("%d. got err = %q, want %q", n, got, tt.err) } } } func noSpace(c rune) rune { if isSpace(byte(c)) { //only used for ascii return -1 } return c } type All struct { Bool bool Int int Int8 int8 Int16 int16 Int32 int32 Int64 int64 Uint uint Uint8 uint8 Uint16 uint16 Uint32 uint32 Uint64 uint64 Uintptr uintptr Float32 float32 Float64 float64 Foo string `json:"bar"` Foo2 string `json:"bar2,dummyopt"` IntStr int64 `json:",string"` PBool *bool PInt *int PInt8 *int8 PInt16 *int16 PInt32 *int32 PInt64 *int64 PUint *uint PUint8 *uint8 PUint16 *uint16 PUint32 *uint32 PUint64 *uint64 PUintptr *uintptr PFloat32 *float32 PFloat64 *float64 String string PString *string Map map[string]Small MapP map[string]*Small PMap *map[string]Small PMapP *map[string]*Small EmptyMap map[string]Small NilMap map[string]Small Slice []Small SliceP []*Small PSlice *[]Small PSliceP *[]*Small EmptySlice []Small NilSlice []Small StringSlice []string ByteSlice []byte Small Small PSmall *Small PPSmall **Small Interface interface{} PInterface *interface{} unexported int } type Small struct { Tag string } var allValue = All{ Bool: true, Int: 2, Int8: 3, Int16: 4, Int32: 5, Int64: 6, Uint: 7, Uint8: 8, Uint16: 9, Uint32: 10, Uint64: 11, Uintptr: 12, Float32: 14.1, Float64: 15.1, Foo: "foo", Foo2: "foo2", IntStr: 42, String: "16", Map: map[string]Small{ "17": {Tag: "tag17"}, "18": {Tag: "tag18"}, }, MapP: map[string]*Small{ "19": {Tag: "tag19"}, "20": nil, }, EmptyMap: map[string]Small{}, Slice: []Small{{Tag: "tag20"}, {Tag: "tag21"}}, SliceP: []*Small{{Tag: "tag22"}, nil, {Tag: "tag23"}}, EmptySlice: []Small{}, StringSlice: []string{"str24", "str25", "str26"}, ByteSlice: []byte{27, 28, 29}, Small: Small{Tag: "tag30"}, PSmall: &Small{Tag: "tag31"}, Interface: 5.2, } var pallValue = All{ PBool: &allValue.Bool, PInt: &allValue.Int, PInt8: &allValue.Int8, PInt16: &allValue.Int16, PInt32: &allValue.Int32, PInt64: &allValue.Int64, PUint: &allValue.Uint, PUint8: &allValue.Uint8, PUint16: &allValue.Uint16, PUint32: &allValue.Uint32, PUint64: &allValue.Uint64, PUintptr: &allValue.Uintptr, PFloat32: &allValue.Float32, PFloat64: &allValue.Float64, PString: &allValue.String, PMap: &allValue.Map, PMapP: &allValue.MapP, PSlice: &allValue.Slice, PSliceP: &allValue.SliceP, PPSmall: &allValue.PSmall, PInterface: &allValue.Interface, } var allValueIndent = `{ "Bool": true, "Int": 2, "Int8": 3, "Int16": 4, "Int32": 5, "Int64": 6, "Uint": 7, "Uint8": 8, "Uint16": 9, "Uint32": 10, "Uint64": 11, "Uintptr": 12, "Float32": 14.1, "Float64": 15.1, "bar": "foo", "bar2": "foo2", "IntStr": "42", "PBool": null, "PInt": null, "PInt8": null, "PInt16": null, "PInt32": null, "PInt64": null, "PUint": null, "PUint8": null, "PUint16": null, "PUint32": null, "PUint64": null, "PUintptr": null, "PFloat32": null, "PFloat64": null, "String": "16", "PString": null, "Map": { "17": { "Tag": "tag17" }, "18": { "Tag": "tag18" } }, "MapP": { "19": { "Tag": "tag19" }, "20": null }, "PMap": null, "PMapP": null, "EmptyMap": {}, "NilMap": null, "Slice": [ { "Tag": "tag20" }, { "Tag": "tag21" } ], "SliceP": [ { "Tag": "tag22" }, null, { "Tag": "tag23" } ], "PSlice": null, "PSliceP": null, "EmptySlice": [], "NilSlice": null, "StringSlice": [ "str24", "str25", "str26" ], "ByteSlice": "Gxwd", "Small": { "Tag": "tag30" }, "PSmall": { "Tag": "tag31" }, "PPSmall": null, "Interface": 5.2, "PInterface": null }` var allValueCompact = strings.Map(noSpace, allValueIndent) var pallValueIndent = `{ "Bool": false, "Int": 0, "Int8": 0, "Int16": 0, "Int32": 0, "Int64": 0, "Uint": 0, "Uint8": 0, "Uint16": 0, "Uint32": 0, "Uint64": 0, "Uintptr": 0, "Float32": 0, "Float64": 0, "bar": "", "bar2": "", "IntStr": "0", "PBool": true, "PInt": 2, "PInt8": 3, "PInt16": 4, "PInt32": 5, "PInt64": 6, "PUint": 7, "PUint8": 8, "PUint16": 9, "PUint32": 10, "PUint64": 11, "PUintptr": 12, "PFloat32": 14.1, "PFloat64": 15.1, "String": "", "PString": "16", "Map": null, "MapP": null, "PMap": { "17": { "Tag": "tag17" }, "18": { "Tag": "tag18" } }, "PMapP": { "19": { "Tag": "tag19" }, "20": null }, "EmptyMap": null, "NilMap": null, "Slice": null, "SliceP": null, "PSlice": [ { "Tag": "tag20" }, { "Tag": "tag21" } ], "PSliceP": [ { "Tag": "tag22" }, null, { "Tag": "tag23" } ], "EmptySlice": null, "NilSlice": null, "StringSlice": null, "ByteSlice": null, "Small": { "Tag": "" }, "PSmall": null, "PPSmall": { "Tag": "tag31" }, "Interface": null, "PInterface": 5.2 }` var pallValueCompact = strings.Map(noSpace, pallValueIndent) func TestRefUnmarshal(t *testing.T) { type S struct { // Ref is defined in encode_test.go. R0 Ref R1 *Ref R2 RefText R3 *RefText } want := S{ R0: 12, R1: new(Ref), R2: 13, R3: new(RefText), } *want.R1 = 12 *want.R3 = 13 var got S if err := Unmarshal([]byte(`{"R0":"ref","R1":"ref","R2":"ref","R3":"ref"}`), &got); err != nil { t.Fatalf("Unmarshal: %v", err) } if !reflect.DeepEqual(got, want) { t.Errorf("got %+v, want %+v", got, want) } } // Test that the empty string doesn't panic decoding when ,string is specified // Issue 3450 func TestEmptyString(t *testing.T) { type T2 struct { Number1 int `json:",string"` Number2 int `json:",string"` } data := `{"Number1":"1", "Number2":""}` dec := NewDecoder(strings.NewReader(data)) var t2 T2 err := dec.Decode(&t2) if err == nil { t.Fatal("Decode: did not return error") } if t2.Number1 != 1 { t.Fatal("Decode: did not set Number1") } } // Test that a null for ,string is not replaced with the previous quoted string (issue 7046). // It should also not be an error (issue 2540, issue 8587). func TestNullString(t *testing.T) { type T struct { A int `json:",string"` B int `json:",string"` C *int `json:",string"` } data := []byte(`{"A": "1", "B": null, "C": null}`) var s T s.B = 1 s.C = new(int) *s.C = 2 err := Unmarshal(data, &s) if err != nil { t.Fatalf("Unmarshal: %v", err) } if s.B != 1 || s.C != nil { t.Fatalf("after Unmarshal, s.B=%d, s.C=%p, want 1, nil", s.B, s.C) } } func intp(x int) *int { p := new(int) *p = x return p } func intpp(x *int) **int { pp := new(*int) *pp = x return pp } var interfaceSetTests = []struct { pre interface{} json string post interface{} }{ {"foo", `"bar"`, "bar"}, {"foo", `2`, 2.0}, {"foo", `true`, true}, {"foo", `null`, nil}, {nil, `null`, nil}, {new(int), `null`, nil}, {(*int)(nil), `null`, nil}, {new(*int), `null`, new(*int)}, {(**int)(nil), `null`, nil}, {intp(1), `null`, nil}, {intpp(nil), `null`, intpp(nil)}, {intpp(intp(1)), `null`, intpp(nil)}, } func TestInterfaceSet(t *testing.T) { for _, tt := range interfaceSetTests { b := struct{ X interface{} }{tt.pre} blob := `{"X":` + tt.json + `}` if err := Unmarshal([]byte(blob), &b); err != nil { t.Errorf("Unmarshal %#q: %v", blob, err) continue } if !reflect.DeepEqual(b.X, tt.post) { t.Errorf("Unmarshal %#q into %#v: X=%#v, want %#v", blob, tt.pre, b.X, tt.post) } } } // JSON null values should be ignored for primitives and string values instead of resulting in an error. // Issue 2540 func TestUnmarshalNulls(t *testing.T) { jsonData := []byte(`{ "Bool" : null, "Int" : null, "Int8" : null, "Int16" : null, "Int32" : null, "Int64" : null, "Uint" : null, "Uint8" : null, "Uint16" : null, "Uint32" : null, "Uint64" : null, "Float32" : null, "Float64" : null, "String" : null}`) nulls := All{ Bool: true, Int: 2, Int8: 3, Int16: 4, Int32: 5, Int64: 6, Uint: 7, Uint8: 8, Uint16: 9, Uint32: 10, Uint64: 11, Float32: 12.1, Float64: 13.1, String: "14"} err := Unmarshal(jsonData, &nulls) if err != nil { t.Errorf("Unmarshal of null values failed: %v", err) } if !nulls.Bool || nulls.Int != 2 || nulls.Int8 != 3 || nulls.Int16 != 4 || nulls.Int32 != 5 || nulls.Int64 != 6 || nulls.Uint != 7 || nulls.Uint8 != 8 || nulls.Uint16 != 9 || nulls.Uint32 != 10 || nulls.Uint64 != 11 || nulls.Float32 != 12.1 || nulls.Float64 != 13.1 || nulls.String != "14" { t.Errorf("Unmarshal of null values affected primitives") } } func TestStringKind(t *testing.T) { type stringKind string var m1, m2 map[stringKind]int m1 = map[stringKind]int{ "foo": 42, } data, err := Marshal(m1) if err != nil { t.Errorf("Unexpected error marshaling: %v", err) } err = Unmarshal(data, &m2) if err != nil { t.Errorf("Unexpected error unmarshaling: %v", err) } if !reflect.DeepEqual(m1, m2) { t.Error("Items should be equal after encoding and then decoding") } } // Custom types with []byte as underlying type could not be marshalled // and then unmarshalled. // Issue 8962. func TestByteKind(t *testing.T) { type byteKind []byte a := byteKind("hello") data, err := Marshal(a) if err != nil { t.Error(err) } var b byteKind err = Unmarshal(data, &b) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(a, b) { t.Errorf("expected %v == %v", a, b) } } // The fix for issue 8962 introduced a regression. // Issue 12921. func TestSliceOfCustomByte(t *testing.T) { type Uint8 uint8 a := []Uint8("hello") data, err := Marshal(a) if err != nil { t.Fatal(err) } var b []Uint8 err = Unmarshal(data, &b) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(a, b) { t.Fatal("expected %v == %v", a, b) } } var decodeTypeErrorTests = []struct { dest interface{} src string }{ {new(string), `{"user": "name"}`}, // issue 4628. {new(error), `{}`}, // issue 4222 {new(error), `[]`}, {new(error), `""`}, {new(error), `123`}, {new(error), `true`}, } func TestUnmarshalTypeError(t *testing.T) { for _, item := range decodeTypeErrorTests { err := Unmarshal([]byte(item.src), item.dest) if _, ok := err.(*UnmarshalTypeError); !ok { t.Errorf("expected type error for Unmarshal(%q, type %T): got %T", item.src, item.dest, err) } } } var unmarshalSyntaxTests = []string{ "tru", "fals", "nul", "123e", `"hello`, `[1,2,3`, `{"key":1`, `{"key":1,`, } func TestUnmarshalSyntax(t *testing.T) { var x interface{} for _, src := range unmarshalSyntaxTests { err := Unmarshal([]byte(src), &x) if _, ok := err.(*SyntaxError); !ok { t.Errorf("expected syntax error for Unmarshal(%q): got %T", src, err) } } } // Test handling of unexported fields that should be ignored. // Issue 4660 type unexportedFields struct { Name string m map[string]interface{} `json:"-"` m2 map[string]interface{} `json:"abcd"` } func TestUnmarshalUnexported(t *testing.T) { input := `{"Name": "Bob", "m": {"x": 123}, "m2": {"y": 456}, "abcd": {"z": 789}}` want := &unexportedFields{Name: "Bob"} out := &unexportedFields{} err := Unmarshal([]byte(input), out) if err != nil { t.Errorf("got error %v, expected nil", err) } if !reflect.DeepEqual(out, want) { t.Errorf("got %q, want %q", out, want) } } // Time3339 is a time.Time which encodes to and from JSON // as an RFC 3339 time in UTC. type Time3339 time.Time func (t *Time3339) UnmarshalJSON(b []byte) error { if len(b) < 2 || b[0] != '"' || b[len(b)-1] != '"' { return fmt.Errorf("types: failed to unmarshal non-string value %q as an RFC 3339 time", b) } tm, err := time.Parse(time.RFC3339, string(b[1:len(b)-1])) if err != nil { return err } *t = Time3339(tm) return nil } func TestUnmarshalJSONLiteralError(t *testing.T) { var t3 Time3339 err := Unmarshal([]byte(`"0000-00-00T00:00:00Z"`), &t3) if err == nil { t.Fatalf("expected error; got time %v", time.Time(t3)) } if !strings.Contains(err.Error(), "range") { t.Errorf("got err = %v; want out of range error", err) } } // Test that extra object elements in an array do not result in a // "data changing underfoot" error. // Issue 3717 func TestSkipArrayObjects(t *testing.T) { json := `[{}]` var dest [0]interface{} err := Unmarshal([]byte(json), &dest) if err != nil { t.Errorf("got error %q, want nil", err) } } // Test semantics of pre-filled struct fields and pre-filled map fields. // Issue 4900. func TestPrefilled(t *testing.T) { ptrToMap := func(m map[string]interface{}) *map[string]interface{} { return &m } // Values here change, cannot reuse table across runs. var prefillTests = []struct { in string ptr interface{} out interface{} }{ { in: `{"X": 1, "Y": 2}`, ptr: &XYZ{X: float32(3), Y: int16(4), Z: 1.5}, out: &XYZ{X: float64(1), Y: float64(2), Z: 1.5}, }, { in: `{"X": 1, "Y": 2}`, ptr: ptrToMap(map[string]interface{}{"X": float32(3), "Y": int16(4), "Z": 1.5}), out: ptrToMap(map[string]interface{}{"X": float64(1), "Y": float64(2), "Z": 1.5}), }, } for _, tt := range prefillTests { ptrstr := fmt.Sprintf("%v", tt.ptr) err := Unmarshal([]byte(tt.in), tt.ptr) // tt.ptr edited here if err != nil { t.Errorf("Unmarshal: %v", err) } if !reflect.DeepEqual(tt.ptr, tt.out) { t.Errorf("Unmarshal(%#q, %s): have %v, want %v", tt.in, ptrstr, tt.ptr, tt.out) } } } var invalidUnmarshalTests = []struct { v interface{} want string }{ {nil, "json: Unmarshal(nil)"}, {struct{}{}, "json: Unmarshal(non-pointer struct {})"}, {(*int)(nil), "json: Unmarshal(nil *int)"}, } func TestInvalidUnmarshal(t *testing.T) { buf := []byte(`{"a":"1"}`) for _, tt := range invalidUnmarshalTests { err := Unmarshal(buf, tt.v) if err == nil { t.Errorf("Unmarshal expecting error, got nil") continue } if got := err.Error(); got != tt.want { t.Errorf("Unmarshal = %q; want %q", got, tt.want) } } } var invalidUnmarshalTextTests = []struct { v interface{} want string }{ {nil, "json: Unmarshal(nil)"}, {struct{}{}, "json: Unmarshal(non-pointer struct {})"}, {(*int)(nil), "json: Unmarshal(nil *int)"}, {new(net.IP), "json: cannot unmarshal string into Go value of type *net.IP"}, } func TestInvalidUnmarshalText(t *testing.T) { buf := []byte(`123`) for _, tt := range invalidUnmarshalTextTests { err := Unmarshal(buf, tt.v) if err == nil { t.Errorf("Unmarshal expecting error, got nil") continue } if got := err.Error(); got != tt.want { t.Errorf("Unmarshal = %q; want %q", got, tt.want) } } } // Test that string option is ignored for invalid types. // Issue 9812. func TestInvalidStringOption(t *testing.T) { num := 0 item := struct { T time.Time `json:",string"` M map[string]string `json:",string"` S []string `json:",string"` A [1]string `json:",string"` I interface{} `json:",string"` P *int `json:",string"` }{M: make(map[string]string), S: make([]string, 0), I: num, P: &num} data, err := Marshal(item) if err != nil { t.Fatalf("Marshal: %v", err) } err = Unmarshal(data, &item) if err != nil { t.Fatalf("Unmarshal: %v", err) } } docker-registry-2.6.2~ds1/vendor/rsc.io/letsencrypt/vendor/gopkg.in/square/go-jose.v1/json/encode.go000066400000000000000000000754611313450123100334560ustar00rootroot00000000000000// Copyright 2010 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package json implements encoding and decoding of JSON objects as defined in // RFC 4627. The mapping between JSON objects and Go values is described // in the documentation for the Marshal and Unmarshal functions. // // See "JSON and Go" for an introduction to this package: // https://golang.org/doc/articles/json_and_go.html package json import ( "bytes" "encoding" "encoding/base64" "fmt" "math" "reflect" "runtime" "sort" "strconv" "strings" "sync" "unicode" "unicode/utf8" ) // Marshal returns the JSON encoding of v. // // Marshal traverses the value v recursively. // If an encountered value implements the Marshaler interface // and is not a nil pointer, Marshal calls its MarshalJSON method // to produce JSON. If no MarshalJSON method is present but the // value implements encoding.TextMarshaler instead, Marshal calls // its MarshalText method. // The nil pointer exception is not strictly necessary // but mimics a similar, necessary exception in the behavior of // UnmarshalJSON. // // Otherwise, Marshal uses the following type-dependent default encodings: // // Boolean values encode as JSON booleans. // // Floating point, integer, and Number values encode as JSON numbers. // // String values encode as JSON strings coerced to valid UTF-8, // replacing invalid bytes with the Unicode replacement rune. // The angle brackets "<" and ">" are escaped to "\u003c" and "\u003e" // to keep some browsers from misinterpreting JSON output as HTML. // Ampersand "&" is also escaped to "\u0026" for the same reason. // // Array and slice values encode as JSON arrays, except that // []byte encodes as a base64-encoded string, and a nil slice // encodes as the null JSON object. // // Struct values encode as JSON objects. Each exported struct field // becomes a member of the object unless // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option. // The empty values are false, 0, any // nil pointer or interface value, and any array, slice, map, or string of // length zero. The object's default key string is the struct field name // but can be specified in the struct field's tag value. The "json" key in // the struct field's tag value is the key name, followed by an optional comma // and options. Examples: // // // Field is ignored by this package. // Field int `json:"-"` // // // Field appears in JSON as key "myName". // Field int `json:"myName"` // // // Field appears in JSON as key "myName" and // // the field is omitted from the object if its value is empty, // // as defined above. // Field int `json:"myName,omitempty"` // // // Field appears in JSON as key "Field" (the default), but // // the field is skipped if empty. // // Note the leading comma. // Field int `json:",omitempty"` // // The "string" option signals that a field is stored as JSON inside a // JSON-encoded string. It applies only to fields of string, floating point, // integer, or boolean types. This extra level of encoding is sometimes used // when communicating with JavaScript programs: // // Int64String int64 `json:",string"` // // The key name will be used if it's a non-empty string consisting of // only Unicode letters, digits, dollar signs, percent signs, hyphens, // underscores and slashes. // // Anonymous struct fields are usually marshaled as if their inner exported fields // were fields in the outer struct, subject to the usual Go visibility rules amended // as described in the next paragraph. // An anonymous struct field with a name given in its JSON tag is treated as // having that name, rather than being anonymous. // An anonymous struct field of interface type is treated the same as having // that type as its name, rather than being anonymous. // // The Go visibility rules for struct fields are amended for JSON when // deciding which field to marshal or unmarshal. If there are // multiple fields at the same level, and that level is the least // nested (and would therefore be the nesting level selected by the // usual Go rules), the following extra rules apply: // // 1) Of those fields, if any are JSON-tagged, only tagged fields are considered, // even if there are multiple untagged fields that would otherwise conflict. // 2) If there is exactly one field (tagged or not according to the first rule), that is selected. // 3) Otherwise there are multiple fields, and all are ignored; no error occurs. // // Handling of anonymous struct fields is new in Go 1.1. // Prior to Go 1.1, anonymous struct fields were ignored. To force ignoring of // an anonymous struct field in both current and earlier versions, give the field // a JSON tag of "-". // // Map values encode as JSON objects. // The map's key type must be string; the map keys are used as JSON object // keys, subject to the UTF-8 coercion described for string values above. // // Pointer values encode as the value pointed to. // A nil pointer encodes as the null JSON object. // // Interface values encode as the value contained in the interface. // A nil interface value encodes as the null JSON object. // // Channel, complex, and function values cannot be encoded in JSON. // Attempting to encode such a value causes Marshal to return // an UnsupportedTypeError. // // JSON cannot represent cyclic data structures and Marshal does not // handle them. Passing cyclic structures to Marshal will result in // an infinite recursion. // func Marshal(v interface{}) ([]byte, error) { e := &encodeState{} err := e.marshal(v) if err != nil { return nil, err } return e.Bytes(), nil } // MarshalIndent is like Marshal but applies Indent to format the output. func MarshalIndent(v interface{}, prefix, indent string) ([]byte, error) { b, err := Marshal(v) if err != nil { return nil, err } var buf bytes.Buffer err = Indent(&buf, b, prefix, indent) if err != nil { return nil, err } return buf.Bytes(), nil } // HTMLEscape appends to dst the JSON-encoded src with <, >, &, U+2028 and U+2029 // characters inside string literals changed to \u003c, \u003e, \u0026, \u2028, \u2029 // so that the JSON will be safe to embed inside HTML