Compare commits

..

1 Commits

Author SHA1 Message Date
Evan Hazlett
c2b5f06209 bump 0.4.0-rc2
Signed-off-by: Evan Hazlett <ejhazlett@gmail.com>
2015-08-04 11:38:18 -04:00
1669 changed files with 94609 additions and 69644 deletions

View File

@@ -1,4 +1,2 @@
docker-machine*
*.log
bin
cover
.git

4
.gitignore vendored
View File

@@ -1,6 +1,2 @@
docker-machine*
*.log
*.iml
.idea/
./bin
cover

View File

@@ -1,10 +1,14 @@
sudo: required
dist: trusty
language: bash
services: docker
env:
matrix:
- TARGET_OS=linux TARGET_ARCH=amd64 TARGETS="make dco fmt lint vet test-long coverage-send"
- TARGET_OS=darwin TARGET_ARCH=amd64 TARGETS="cross"
- TARGET_OS=windows TARGET_ARCH=amd64 TARGETS="cross"
script: USE_CONTAINER=true make "$TARGETS"
language: go
sudo: false
go:
- 1.3
install:
- export GOPATH=${TRAVIS_BUILD_DIR}/Godeps/_workspace:$GOPATH
- export PATH=${TRAVIS_BUILD_DIR}/Godeps/_workspace/bin:$PATH
- go get -t -v ./...
- go get code.google.com/p/go.tools/cmd/cover
script:
- script/validate-dco
- script/validate-gofmt
- go test -v -short ./...
- script/generate-coverage

View File

@@ -1,157 +1,78 @@
# Changelog
# 0.5.1 (2015-11-16)
- Fixed boot2docker VM import regression
- Fix regression breaking `docker-machine env -u` to unset environment variables
- Enhanced virtualization capability detection and `VBoxManage` path detection
- Properly lock VirtualBox access when running several commands concurrently
- Allow plugins to write to STDOUT without `--debug` enabled
- Fix Rackspace driver regression
- Support colons in `docker-machine scp` filepaths
- Pass environment variables for provisioned Engines to Swarm as well
- Various enhancements around boot2docker ISO upgrade (progress bar, increased timeout)
# 0.5.0 (2015-11-1)
- General
- Add pluggable driver model
- Clean up code to be more modular and reusable in `libmachine`
- Add `--github-api-token` for situations where users are getting rate limited
by GitHub attempting to get the current `boot2docker.iso` version
- Various enhancements around the Makefile and build toolchain (still an active WIP)
- Disable SSH multiplex explicitly in commands run with the "External" client
- Show "-" for "inactive" machines instead of nothing
- Make daemon status detection more robust
- Provisioners
- New CoreOS, SUSE, and Arch Linux provisioners
- Fixes around package installation / upgrade code on Debian and Ubuntu
- CLI
- Support for regular expression pattern matching and matching by names in `ls --filter`
- `--no-proxy` flag for `env` (sets `NO_PROXY` in addition to other environment variables)
- Drivers
- `openstack`
- `--openstack-ip-version` parameter
- `--openstack-active-timeout` parameter
- `google`
- fix destructive behavior of `start` / `stop`
- `hyperv`
- fix issues with PowerShell
- `vmwarefusion`
- some issues with shared folders fixed
- `--vmwarefusion-configdrive-url` option for configuration via `cloud-init`
- `amazonec2`
- `--amazonec2-use-private-address` option to use private networking
- `virtualbox`
- Enhancements around robustness of the created host-only network
- Fix IPv6 network mask prefix parsing
- `--virtualbox-no-share` option to disable the automatic home directory mount
- `--virtualbox-hostonly-nictype` and `--virtualbox-hostonly-nicpromisc` for controlling settings around the created hostonly NIC
# 0.4.1 (2015-08)
- Fixes `upgrade` functionality on Debian based systems
- Fixes `upgrade` functionality on Ubuntu based systems
# 0.4.0 (2015-08-11)
## Updates
- HTTP Proxy support for Docker Engine
- RedHat distros now use Docker Yum repositories
- Ability to set environment variables in the Docker Engine
- Internal libmachine updates for stability
## Drivers
- Google:
- Preemptible instances
- Static IP support
## Fixes
- Swarm Discovery Flag is verified
- Timeout added to `ls` command to prevent hangups
- SSH command failure now reports information about error
- Configuration migration updates
Changelog
==========
# 0.3.0 (2015-06-18)
## Features
- Engine option configuration (ability to configure all engine options)
- Swarm option configuration (ability to configure all swarm options)
- New Provisioning system to allow for greater flexibility and stability for installing and configuring Docker
- New Provisioners
- Rancher OS
- RedHat Enterprise Linux 7.0+ (experimental)
- Fedora 21+ (experimental)
- Debian 8+ (experimental)
- PowerShell support (configure Windows Docker CLI)
- Command Prompt (cmd.exe) support (configure Windows Docker CLI)
- Filter command help by driver
- Ability to import Boot2Docker instances
- Boot2Docker CLI migration guide (experimental)
- Format option for `inspect` command
- New logging output format to improve readability and display across platforms
- Updated "active" machine concept - now is implicit according to `DOCKER_HOST` environment variable. Note: this removes the implicit "active" machine and can no longer be specified with the `active` command. You change the "active" host by using the `env` command instead.
- Specify Swarm version (`--swarm-image` flag)
- Engine option configuration (ability to configure all engine options)
- Swarm option configuration (ability to configure all swarm options)
- New Provisioning system to allow for greater flexibility and stability for installing and configuring Docker
- New Provisioners
- Rancher OS
- RedHat Enterprise Linux 7.0+ (experimental)
- Fedora 21+ (experimental)
- Debian 8+ (experimental)
- PowerShell support (configure Windows Docker CLI)
- Command Prompt (cmd.exe) support (configure Windows Docker CLI)
- Filter command help by driver
- Ability to import Boot2Docker instances
- Boot2Docker CLI migration guide (experimental)
- Format option for `inspect` command
- New logging output format to improve readability and display across platforms
- Updated "active" machine concept - now is implicit according to `DOCKER_HOST` environment variable. Note: this removes the implicit "active" machine and can no longer be specified with the `active` command. You change the "active" host by using the `env` command instead.
- Specify Swarm version (`--swarm-image` flag)
## Drivers
- New: Exoscale Driver
- New: Generic Driver (provision any host with supported base OS and SSH)
- Amazon EC2
- SSH user is configurable
- Support for Spot instances
- Add option to use private address only
- Base AMI updated to 20150417
- Google
- Support custom disk types
- Updated base image to v20150316
- Openstack
- Support for Keystone v3 domains
- Rackspace
- Misc fixes including environment variable for Flavor Id and stability
- Softlayer
- Enable local disk as provisioning option
- Fixes for SSH access errors
- Fixed bug where public IP would always be returned when requesting private
- Add support for specifying public and private VLAN IDs
- VirtualBox
- Use Intel network interface driver (adds great stability)
- Stability fixes for NAT access
- Use DNS pass through
- Default CPU to single core for improved performance
- Enable shared folder support for Windows hosts
- VMware Fusion
- Boot2Docker ISO updated
- Shared folder support
- New: Exoscale Driver
- New: Generic Driver (provision any host with supported base OS and SSH)
- Amazon EC2
- SSH user is configurable
- Support for Spot instances
- Add option to use private address only
- Base AMI updated to 20150417
- Google
- Support custom disk types
- Updated base image to v20150316
- Openstack
- Support for Keystone v3 domains
- Rackspace
- Misc fixes including environment variable for Flavor Id and stability
- Softlayer
- Enable local disk as provisioning option
- Fixes for SSH access errors
- Fixed bug where public IP would always be returned when requesting private
- Add support for specifying public and private VLAN IDs
- VirtualBox
- Use Intel network interface driver (adds great stability)
- Stability fixes for NAT access
- Use DNS pass through
- Default CPU to single core for improved performance
- Enable shared folder support for Windows hosts
- VMware Fusion
- Boot2Docker ISO updated
- Shared folder support
## Fixes
- Provisioning improvements to ensure Docker is available
- SSH improvements for provisioning stability
- Fixed SSH key generation bug on Windows
- Help formatting for improved readability
- Provisioning improvements to ensure Docker is available
- SSH improvements for provisioning stability
- Fixed SSH key generation bug on Windows
- Help formatting for improved readability
## Breaking Changes
- "Short-Form" name reference no longer supported Instead of "docker-machine " implying the active host you must now use docker-machine
- VMware shared folders require Boot2Docker 1.7
- "Short-Form" name reference no longer supported Instead of "docker-machine " implying the active host you must now use docker-machine
- VMware shared folders require Boot2Docker 1.7
## Special Thanks
## Special Thanks
We would like to thank all contributors. Machine would not be where it is
without you. We would also like to give special thanks to the following
contributors for outstanding contributions to the project:
- @frapposelli for VMware updates and fixes
- @hairyhenderson for several improvements to Softlayer driver, inspect formatting and lots of fixes
- @ibuildthecloud for rancher os provisioning
- @sthulb for portable SSH library
- @vincentbernat for exoscale
- @zchee for Amazon updates and great doc updates
- @frapposelli for VMware updates and fixes
- @hairyhenderson for several improvements to Softlayer driver, inspect formatting and lots of fixes
- @ibuildthecloud for rancher os provisioning
- @sthulb for portable SSH library
- @vincentbernat for exoscale
- @zchee for Amazon updates and great doc updates
# 0.2.0 (2015-04-16)
@@ -159,85 +80,85 @@ Core Stability and Driver Updates
## Core
- Support for system proxy environment
- New command to regenerate TLS certificates
- Note: this will restart the Docker engine to apply
- Updates to driver operations (create, start, stop, etc) for better reliability
- New internal `libmachine` package for internal api (not ready for public usage)
- Updated Driver Interface
- [Driver Spec](https://github.com/docker/machine/blob/master/docs/DRIVER_SPEC.md)
- Removed host provisioning from Drivers to enable a more consistent install
- Removed SSH commands from each Driver for more consistent operations
- Swarm: machine now uses Swarm default binpacking strategy
- Support for system proxy environment
- New command to regenerate TLS certificates
- Note: this will restart the Docker engine to apply
- Updates to driver operations (create, start, stop, etc) for better reliability
- New internal `libmachine` package for internal api (not ready for public usage)
- Updated Driver Interface
- [Driver Spec](https://github.com/docker/machine/blob/master/docs/DRIVER_SPEC.md)
- Removed host provisioning from Drivers to enable a more consistent install
- Removed SSH commands from each Driver for more consistent operations
- Swarm: machine now uses Swarm default binpacking strategy
## Driver Updates
- All drivers updated to new Driver interface
- Amazon EC2
- Better checking for subnets on creation
- Support for using Private IPs in VPC
- Fixed bug with duplicate security group authorization with Swarm
- Support for IAM instance profile
- Fixed bug where IP was not properly detected upon stop
- DigitalOcean
- IPv6 support
- Backup option
- Private Networking
- Openstack / Rackspace
- Gophercloud updated to latest version
- New insecure flag to disable TLS (use with caution)
- Google
- Google source image updated
- Ability to specify auth token via file
- VMware Fusion
- Paravirtualized driver for disk (pvscsi)
- Enhanced paravirtualized NIC (vmxnet3)
- Power option updates
- SSH keys persistent across reboots
- Stop now gracefully stops VM
- vCPUs now match host CPUs
- SoftLayer
- Fixed provision bug where `curl` was not present
- VirtualBox
- Correct power operations with Saved VM state
- Fixed bug where image option was ignored
- All drivers updated to new Driver interface
- Amazon EC2
- Better checking for subnets on creation
- Support for using Private IPs in VPC
- Fixed bug with duplicate security group authorization with Swarm
- Support for IAM instance profile
- Fixed bug where IP was not properly detected upon stop
- DigitalOcean
- IPv6 support
- Backup option
- Private Networking
- Openstack / Rackspace
- Gophercloud updated to latest version
- New insecure flag to disable TLS (use with caution)
- Google
- Google source image updated
- Ability to specify auth token via file
- VMware Fusion
- Paravirtualized driver for disk (pvscsi)
- Enhanced paravirtualized NIC (vmxnet3)
- Power option updates
- SSH keys persistent across reboots
- Stop now gracefully stops VM
- vCPUs now match host CPUs
- SoftLayer
- Fixed provision bug where `curl` was not present
- VirtualBox
- Correct power operations with Saved VM state
- Fixed bug where image option was ignored
## CLI
- Auto-regeneration of TLS certificates when TLS error is detected
- Note: this will restart the Docker engine to apply
- Minor UI updates including improved sorting and updated command docs
- Bug with `config` and `env` with spaces fixed
- Note: you now must use `eval $(docker-machine env machine)` to load environment settings
- Updates to better support `fish` shell
- Use `--tlsverify` for both `config` and `env` commands
- Commands now use eval for better interoperability with shell
- Auto-regeneration of TLS certificates when TLS error is detected
- Note: this will restart the Docker engine to apply
- Minor UI updates including improved sorting and updated command docs
- Bug with `config` and `env` with spaces fixed
- Note: you now must use `eval $(docker-machine env machine)` to load environment settings
- Updates to better support `fish` shell
- Use `--tlsverify` for both `config` and `env` commands
- Commands now use eval for better interoperability with shell
## Testing
- New integration test framework (bats)
- New integration test framework (bats)
# 0.1.0 (2015-02-26)
Initial beta release.
- Provision Docker Engines using multiple drivers
- Provide light management for the machines
- Create, Start, Stop, Restart, Kill, Remove, SSH
- Configure the Docker Engine for secure communication (TLS)
- Easily switch target machine for fast configuration of Docker Engine client
- Provision Swarm clusters (experimental)
- Provision Docker Engines using multiple drivers
- Provide light management for the machines
- Create, Start, Stop, Restart, Kill, Remove, SSH
- Configure the Docker Engine for secure communication (TLS)
- Easily switch target machine for fast configuration of Docker Engine client
- Provision Swarm clusters (experimental)
## Included drivers
- Amazon EC2
- Digital Ocean
- Google
- Microsoft Azure
- Microsoft Hyper-V
- Openstack
- Rackspace
- VirtualBox
- VMware Fusion
- VMware vCloud Air
- VMware vSphere
- Amazon EC2
- Digital Ocean
- Google
- Microsoft Azure
- Microsoft Hyper-V
- Openstack
- Rackspace
- VirtualBox
- VMware Fusion
- VMware vCloud Air
- VMware vSphere

View File

@@ -2,7 +2,6 @@
[![GoDoc](https://godoc.org/github.com/docker/machine?status.png)](https://godoc.org/github.com/docker/machine)
[![Build Status](https://travis-ci.org/docker/machine.svg?branch=master)](https://travis-ci.org/docker/machine)
[![Coverage Status](https://coveralls.io/repos/docker/machine/badge.svg?branch=upstream-master&service=github)](https://coveralls.io/github/docker/machine?branch=upstream-master)
Want to hack on Machine? Awesome! Here are instructions to get you
started.
@@ -11,165 +10,106 @@ Machine is a part of the [Docker](https://www.docker.com) project, and follows
the same rules and principles. If you're already familiar with the way
Docker does things, you'll feel right at home.
Otherwise, please read [Docker's contributions
guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md).
# Building
Otherwise, go read
[Docker's contributions guidelines](https://github.com/docker/docker/blob/master/CONTRIBUTING.md).
The requirements to build Machine are:
1. A running instance of Docker or a Golang 1.5 development environment
2. The `bash` shell
3. [Make](https://www.gnu.org/software/make/)
1. A running instance of Docker
2. The `bash` shell
## Build using Docker containers
To build, run:
To build the `docker-machine` binary using containers, simply run:
$ script/build
$ export USE_CONTAINER=true
$ make build
From the Machine repository's root. Machine will run the build inside of a
Docker container and the compiled binaries will appear in the project directory
on the host.
## Local Go 1.5 development environment
By default, Machine will run a build which cross-compiles binaries for a variety
of architectures and operating systems. If you know that you are only compiling
for a particular architecture and/or operating system, you can speed up
compilation by overriding the default argument that the build script passes
to [gox](https://github.com/mitchellh/gox). This is very useful if you want
to iterate quickly on a new feature, bug fix, etc.
Make sure the source code directory is under a correct directory structure to use Go 1.5 vendoring;
example of cloning and preparing the correct environment `GOPATH`:
For instance, if you only want to compile for use on OS X with the x86_64 arch,
run:
mkdir docker-machine
cd docker-machine
export GOPATH="$PWD"
go get github.com/docker/machine
cd src/github.com/docker/machine
$ script/build -osarch="darwin/amd64"
At this point, simply run:
If you don't need to run the `docker build` to generate the image on each
compile, i.e. if you have built the image already, you can skip the image build
using the `SKIP_BUILD` environment variable, for instance:
$ make build
$ SKIP_BUILD=1 script/build -osarch="darwin/amd64"
## Built binary
If you have any questions we're in #docker-machine on Freenode.
After the build is complete a `bin/docker-machine` binary will be created.
## Unit Tests
You may call:
To run the unit tests for the whole project, using the following script:
$ make clean
$ script/test
to clean-up build results.
This will run the unit tests inside of a container, so you don't have to worry
about configuring your environment properly before doing so.
## Tests and validation
To run the unit tests for only a specific subdirectory of the project, you can
pass an argument to that script to specify which directory, e.g.:
We use the usual `go` tools for this, to run those commands you need at least the linter which you can
install with `go get -u github.com/golang/lint/golint`
To run basic validation (dco, fmt), and the project unit tests, call:
$ make test
If you want more indepth validation (vet, lint), and all tests with race detection, call:
$ make validate
$ script/test ./drivers/amazonec2
If you make a pull request, it is highly encouraged that you submit tests for
the code that you have added or modified in the same pull request.
## Code Coverage
To generate an html code coverage report of the Machine codebase, run:
Machine includes a script to check for missing `*_test.go` files and to generate
an [HTML-based representation of which code is covered by tests](http://blog.golang.org/cover#TOC_5.).
make coverage-serve
To run the code coverage script, execute:
And navigate to <http://localhost:8000> (hit `CTRL+C` to stop the server).
```console
$ ./script/coverage serve
```
### Native build
You will see the results of the code coverage check as they come in.
Alternatively, if you are building natively, you can simply run:
make coverage-html
This will generate and open the report file:
This will also generate the code coverage website and serve it from a container
on port 8000. By default, `/` will show you the source files from the base
directory, and you can navigate to the coverage for any particular subdirectory
of the Docker Machine repo's root by going to that path. For instance, to see
the coverage for the VirtualBox driver's package, browse to `/drivers/virtualbox`.
![](/docs/img/coverage.png)
## List of all targets
### High-level targets
make clean
make build
make test
make validate
### Advanced build targets
Just build the machine binary itself (native):
make machine
Just build the plugins (native):
make plugins
Build for all supported oses and architectures (binaries will be in the `bin` project subfolder):
make cross
Build for a specific list of oses and architectures:
TARGET_OS=linux TARGET_ARCH="amd64 arm" make cross
You can further control build options through the following environment variables:
DEBUG=true # enable debug build
STATIC=true # build static (note: when cross-compiling, the build is always static)
VERBOSE=true # verbose output
PREFIX=folder # put binaries in another folder (not the default `./bin`)
Scrub build results:
make build-clean
### Coverage targets
make coverage-html
make coverage-serve
make coverage-send
make coverage-generate
make coverage-clean
### Tests targets
make test-short
make test-long
make test-integration
### Validation targets
make fmt
make vet
make lint
make dco
### Save and restore dependencies
make dep-save
make dep-restore
You can hit `CTRL+C` to stop the server.
## Integration Tests
### Setup
We use [BATS](https://github.com/sstephenson/bats) for integration testing, so,
first make sure to [install it](https://github.com/sstephenson/bats#installing-bats-from-source).
We utilize [BATS](https://github.com/sstephenson/bats) for integration testing.
This runs tests against the generated binary. To use, first make sure to
[install BATS](https://github.com/sstephenson/bats). Then run `./script/build`
to generate the binary for your system.
### Basic Usage
You first need to build, calling `make build`.
Once you have the binary, the integration tests can be invoked using the
`test/integration/run-bats.sh` wrapper script.
You can then invoke integration tests calling `DRIVER=foo make test-integration TESTSUITE`, where `TESTSUITE` is
one of the `test/integration` subfolder, and `foo` is the specific driver you want to test.
Using this wrapper script, you can invoke a test or subset of tests for a
particular driver. To set the driver, use the `DRIVER` environment variable.
Examples:
The following examples are all shown relative to the project's root directory,
but you should be able to invoke them from any directory without issue.
To invoke just one test:
```console
$ DRIVER=virtualbox make test-integration test/integration/core/core-commands.bats
$ DRIVER=virtualbox ./test/integration/run-bats.sh test/integration/core/core-commands.bats
✓ virtualbox: machine should not exist
✓ virtualbox: create
✓ virtualbox: ls
@@ -193,13 +133,26 @@ Cleaning up machines...
Successfully removed bats-virtualbox-test
```
To invoke a shared test with a different driver:
```console
$ DRIVER=digitalocean ./test/integration/run-bats.sh test/integration/core/core-commands.bats
...
```
To invoke a directory of tests recursively:
```console
$ DRIVER=virtualbox make test-integration test/integration/core/
$ DRIVER=virtualbox ./test/integration/run-bats.sh test/integration/core/
...
```
If you want to invoke a group of tests across two or more different drivers at
once (e.g. every test in the `drivers` directory), at the time of writing there
is no first-class support to do so - you will have to write your own wrapper
scripts, bash loops, etc. However, in the future, this may gain first-class
support as usage patterns become more clear.
### Extra Create Arguments
In some cases, for instance to test the creation of a specific base OS (e.g.
@@ -210,13 +163,15 @@ Keep in mind that Machine supports environment variables for many of these
flags. So, for instance, you could run the command (substituting, of course,
the proper secrets):
$ DRIVER=amazonec2 \
AWS_VPC_ID=vpc-xxxxxxx \
AWS_SECRET_ACCESS_KEY=yyyyyyyyyyyyy \
AWS_ACCESS_KEY_ID=zzzzzzzzzzzzzzzz \
AWS_AMI=ami-12663b7a \
AWS_SSH_USER=ec2-user \
make test-integration test/integration/core
```
$ DRIVER=amazonec2 \
AWS_VPC_ID=vpc-xxxxxxx \
AWS_SECRET_ACCESS_KEY=yyyyyyyyyyyyy \
AWS_ACCESS_KEY_ID=zzzzzzzzzzzzzzzz \
AWS_AMI=ami-12663b7a \
AWS_SSH_USER=ec2-user \
./test/integration/run-bats.sh test/integration/core
```
in order to run the core tests on Red Hat Enterprise Linux on Amazon.
@@ -228,11 +183,11 @@ guide you.
At the time of writing, there is:
1. A `core` directory which contains tests that are applicable to all drivers.
2. A `drivers` directory which contains tests that are applicable only to
specific drivers with sub-directories for each provider.
3. A `cli` directory which is meant for testing functionality of the command
line interface, without much regard for driver-specific details.
1. A `core` directory which contains tests that are applicable to all drivers.
2. A `drivers` directory which contains tests that are applicable only to
specific drivers with sub-directories for each provider.
3. A `cli` directory which is meant for testing functionality of the command
line interface, without much regard for driver-specific details.
### Guidelines
@@ -241,15 +196,15 @@ work in progress, but here are some general guidelines from the maintainers:
1. Ideally, each test file should have only one concern.
2. Tests generally should not spin up more than one machine unless the test is
deliberately testing something which involves multiple machines, such as an `ls`
test which involves several machines, or a test intended to create and check
some property of a Swarm cluster.
deliberately testing something which involves multiple machines, such as an `ls`
test which involves several machines, or a test intended to create and check
some property of a Swarm cluster.
3. BATS will print the output of commands executed during a test if the test
fails. This can be useful, for instance to dump the magic `$output` variable
that BATS provides and/or to get debugging information.
fails. This can be useful, for instance to dump the magic `$output` variable
that BATS provides and/or to get debugging information.
4. It is not strictly needed to clean up the machines as part of the test. The
BATS wrapper script has a hook to take care of cleaning up all created machines
after each test.
BATS wrapper script has a hook to take care of cleaning up all created machines
after each test.
# Drivers
@@ -257,11 +212,11 @@ Docker Machine has several included drivers that supports provisioning hosts
in various providers. If you wish to contribute a driver, we ask the following
to ensure we keep the driver in a consistent and stable state:
- Address issues filed against this driver in a timely manner
- Review PRs for the driver
- Be responsible for maintaining the infrastructure to run unit tests
and integration tests on the new supported environment
- Participate in a weekly driver maintainer meeting
- Address issues filed against this driver in a timely manner
- Review PRs for the driver
- Be responsible for maintaining the infrastructure to run unit tests
and integration tests on the new supported environment
- Participate in a weekly driver maintainer meeting
If you can commit to those, the next step is to make sure the driver adheres
to the [spec](https://github.com/docker/machine/blob/master/docs/DRIVER_SPEC.md).

View File

@@ -1,13 +1,15 @@
FROM golang:1.5.1
FROM golang:1.4.2-cross
RUN go get github.com/golang/lint/golint \
github.com/mattn/goveralls \
golang.org/x/tools/cover \
github.com/tools/godep \
github.com/aktau/github-release
# TODO: Vendor these `go get` commands using Godep.
RUN go get github.com/mitchellh/gox
RUN go get github.com/aktau/github-release
RUN go get github.com/tools/godep
RUN go get golang.org/x/tools/cmd/cover
ENV GOPATH /go/src/github.com/docker/machine/Godeps/_workspace:/go
ENV MACHINE_BINARY /go/src/github.com/docker/machine/docker-machine
ENV USER root
WORKDIR /go/src/github.com/docker/machine
ADD . /go/src/github.com/docker/machine
RUN mkdir bin

183
Godeps/Godeps.json generated
View File

@@ -1,57 +1,12 @@
{
"ImportPath": "github.com/docker/machine",
"GoVersion": "go1.5.1",
"Packages": [
"github.com/docker/machine",
"github.com/docker/machine/cmd",
"github.com/docker/machine/commands",
"github.com/docker/machine/commands/mcndirs",
"github.com/docker/machine/drivers/amazonec2",
"github.com/docker/machine/drivers/amazonec2/amz",
"github.com/docker/machine/drivers/azure",
"github.com/docker/machine/drivers/digitalocean",
"github.com/docker/machine/drivers/errdriver",
"github.com/docker/machine/drivers/exoscale",
"github.com/docker/machine/drivers/fakedriver",
"github.com/docker/machine/drivers/generic",
"github.com/docker/machine/drivers/google",
"github.com/docker/machine/drivers/hyperv",
"github.com/docker/machine/drivers/none",
"github.com/docker/machine/drivers/openstack",
"github.com/docker/machine/drivers/rackspace",
"github.com/docker/machine/drivers/softlayer",
"github.com/docker/machine/drivers/virtualbox",
"github.com/docker/machine/drivers/vmwarefusion",
"github.com/docker/machine/drivers/vmwarevcloudair",
"github.com/docker/machine/drivers/vmwarevsphere",
"github.com/docker/machine/drivers/vmwarevsphere/errors",
"github.com/docker/machine/libmachine",
"github.com/docker/machine/libmachine/auth",
"github.com/docker/machine/libmachine/cert",
"github.com/docker/machine/libmachine/drivers",
"github.com/docker/machine/libmachine/drivers/plugin",
"github.com/docker/machine/libmachine/drivers/plugin/localbinary",
"github.com/docker/machine/libmachine/drivers/rpc",
"github.com/docker/machine/libmachine/engine",
"github.com/docker/machine/libmachine/examples",
"github.com/docker/machine/libmachine/host",
"github.com/docker/machine/libmachine/hosttest",
"github.com/docker/machine/libmachine/log",
"github.com/docker/machine/libmachine/mcnerror",
"github.com/docker/machine/libmachine/mcnflag",
"github.com/docker/machine/libmachine/mcnutils",
"github.com/docker/machine/libmachine/persist",
"github.com/docker/machine/libmachine/provider",
"github.com/docker/machine/libmachine/provision",
"github.com/docker/machine/libmachine/provision/pkgaction",
"github.com/docker/machine/libmachine/provision/serviceaction",
"github.com/docker/machine/libmachine/ssh",
"github.com/docker/machine/libmachine/state",
"github.com/docker/machine/libmachine/swarm",
"github.com/docker/machine/libmachine/version",
"github.com/docker/machine/version"
],
"GoVersion": "go1.4.2",
"Deps": [
{
"ImportPath": "code.google.com/p/goauth2/oauth",
"Comment": "weekly-56",
"Rev": "afe77d958c701557ec5dc56f6936fcc194d15520"
},
{
"ImportPath": "github.com/MSOpenTech/azure-sdk-for-go",
"Comment": "v1.1-17-g515f3ec",
@@ -63,19 +18,93 @@
},
{
"ImportPath": "github.com/codegangsta/cli",
"Comment": "1.2.0-179-g0302d39",
"Rev": "0302d3914d2a6ad61404584cdae6e6dbc9c03599"
"Comment": "1.2.0-64-ge1712f3",
"Rev": "e1712f381785e32046927f64a7c86fe569203196"
},
{
"ImportPath": "github.com/digitalocean/godo",
"Comment": "v0.9.0-8-g2124bf3",
"Rev": "2124bf3eeeb4ac070337bb19ef7b76a745de56f4"
"Comment": "v0.5.0",
"Rev": "5478aae80694de1d2d0e02c386bbedd201266234"
},
{
"ImportPath": "github.com/docker/docker/dockerversion",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/engine",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/archive",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/fileutils",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/ioutils",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/mflag",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/parsers",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/pools",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/promise",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/system",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/term",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/timeutils",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/units",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/pkg/version",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/docker/vendor/src/code.google.com/p/go/src/pkg/archive/tar",
"Comment": "v1.5.0",
"Rev": "a8a31eff10544860d2188dddabdee4d727545796"
},
{
"ImportPath": "github.com/docker/libtrust",
"Rev": "c54fbb67c1f1e68d7d6f8d2ad7c9360404616a41"
},
{
"ImportPath": "github.com/google/go-querystring/query",
"Rev": "30f7a39f4a218feb5325f3aebc60c32a572a8274"
@@ -84,18 +113,14 @@
"ImportPath": "github.com/mitchellh/mapstructure",
"Rev": "740c764bc6149d3f1806231418adb9f52c11bcbf"
},
{
"ImportPath": "github.com/pyr/egoscale/src/egoscale",
"Rev": "bbaa67324aeeacc90430c1fe0a9c620d3929512e"
},
{
"ImportPath": "github.com/rackspace/gophercloud",
"Comment": "v1.0.0-558-gce0f487",
"Comment": "v1.0.0-558-ce0f487",
"Rev": "ce0f487f6747ab43c4e4404722df25349385bebd"
},
{
"ImportPath": "github.com/skarademir/naturalsort",
"Rev": "69a5d87bef620f77ee8508db30c846b3b84b111e"
"Rev": "983d4d86054d80f91fd04dd62ec52c1d078ce403"
},
{
"ImportPath": "github.com/smartystreets/go-aws-auth",
@@ -105,6 +130,10 @@
"ImportPath": "github.com/stretchr/testify/assert",
"Rev": "e4ec8152c15fc46bd5056ce65997a07c7d415325"
},
{
"ImportPath": "github.com/pyr/egoscale/src/egoscale",
"Rev": "bbaa67324aeeacc90430c1fe0a9c620d3929512e"
},
{
"ImportPath": "github.com/tent/http-link-go",
"Rev": "ac974c61c2f990f4115b119354b5e0b47550e888"
@@ -114,45 +143,21 @@
"Comment": "v0.0.2",
"Rev": "66a23eaabc61518f91769939ff541886fe1dceef"
},
{
"ImportPath": "golang.org/x/crypto/curve25519",
"Rev": "beef0f4390813b96e8e68fd78570396d0f4751fc"
},
{
"ImportPath": "golang.org/x/crypto/ssh",
"Rev": "beef0f4390813b96e8e68fd78570396d0f4751fc"
"Rev": "1fbbd62cfec66bd39d91e97749579579d4d3037e"
},
{
"ImportPath": "golang.org/x/net/context",
"Rev": "4f2fc6c1e69d41baf187332ee08fbd2b296f21ed"
},
{
"ImportPath": "golang.org/x/oauth2",
"Rev": "442624c9ec9243441e83b374a9e22ac549b5c51d"
},
{
"ImportPath": "golang.org/x/sys/windows/registry",
"Rev": "d9157a9621b69ad1d8d77a1933590c416593f24f"
"Rev": "d9558e5c97f85372afee28cf2b6059d7d3818919"
},
{
"ImportPath": "google.golang.org/api/compute/v1",
"Rev": "030d584ade5f79aa2ed0ce067e8f7da50c9a10d5"
},
{
"ImportPath": "google.golang.org/api/gensupport",
"Rev": "030d584ade5f79aa2ed0ce067e8f7da50c9a10d5"
"Rev": "a09229c13c2f13bbdedf7b31b506cad4c83ef3bf"
},
{
"ImportPath": "google.golang.org/api/googleapi",
"Rev": "030d584ade5f79aa2ed0ce067e8f7da50c9a10d5"
},
{
"ImportPath": "google.golang.org/cloud/compute/metadata",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
},
{
"ImportPath": "google.golang.org/cloud/internal",
"Rev": "975617b05ea8a58727e6c1a06b6161ff4185a9f2"
"Rev": "a09229c13c2f13bbdedf7b31b506cad4c83ef3bf"
}
]
}

2
Godeps/_workspace/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,2 @@
/pkg
/bin

View File

@@ -0,0 +1,100 @@
// Copyright 2011 The goauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This program makes a call to the specified API, authenticated with OAuth2.
// a list of example APIs can be found at https://code.google.com/oauthplayground/
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"code.google.com/p/goauth2/oauth"
)
var (
clientId = flag.String("id", "", "Client ID")
clientSecret = flag.String("secret", "", "Client Secret")
scope = flag.String("scope", "https://www.googleapis.com/auth/userinfo.profile", "OAuth scope")
redirectURL = flag.String("redirect_url", "oob", "Redirect URL")
authURL = flag.String("auth_url", "https://accounts.google.com/o/oauth2/auth", "Authentication URL")
tokenURL = flag.String("token_url", "https://accounts.google.com/o/oauth2/token", "Token URL")
requestURL = flag.String("request_url", "https://www.googleapis.com/oauth2/v1/userinfo", "API request")
code = flag.String("code", "", "Authorization Code")
cachefile = flag.String("cache", "cache.json", "Token cache file")
)
const usageMsg = `
To obtain a request token you must specify both -id and -secret.
To obtain Client ID and Secret, see the "OAuth 2 Credentials" section under
the "API Access" tab on this page: https://code.google.com/apis/console/
Once you have completed the OAuth flow, the credentials should be stored inside
the file specified by -cache and you may run without the -id and -secret flags.
`
func main() {
flag.Parse()
// Set up a configuration.
config := &oauth.Config{
ClientId: *clientId,
ClientSecret: *clientSecret,
RedirectURL: *redirectURL,
Scope: *scope,
AuthURL: *authURL,
TokenURL: *tokenURL,
TokenCache: oauth.CacheFile(*cachefile),
}
// Set up a Transport using the config.
transport := &oauth.Transport{Config: config}
// Try to pull the token from the cache; if this fails, we need to get one.
token, err := config.TokenCache.Token()
if err != nil {
if *clientId == "" || *clientSecret == "" {
flag.Usage()
fmt.Fprint(os.Stderr, usageMsg)
os.Exit(2)
}
if *code == "" {
// Get an authorization code from the data provider.
// ("Please ask the user if I can access this resource.")
url := config.AuthCodeURL("")
fmt.Print("Visit this URL to get a code, then run again with -code=YOUR_CODE\n\n")
fmt.Println(url)
return
}
// Exchange the authorization code for an access token.
// ("Here's the code you gave the user, now give me a token!")
token, err = transport.Exchange(*code)
if err != nil {
log.Fatal("Exchange:", err)
}
// (The Exchange method will automatically cache the token.)
fmt.Printf("Token is cached in %v\n", config.TokenCache)
}
// Make the actual request using the cached token to authenticate.
// ("Here's the token, let me in!")
transport.Token = token
// Make the request.
r, err := transport.Client().Get(*requestURL)
if err != nil {
log.Fatal("Get:", err)
}
defer r.Body.Close()
// Write the response to standard output.
io.Copy(os.Stdout, r.Body)
// Send final carriage return, just to be neat.
fmt.Println()
}

View File

@@ -0,0 +1 @@
{"web":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"XXXXXXXXXXXX@developer.gserviceaccount.com","client_x509_cert_url":"https://www.googleapis.com/robot/v1/metadata/x509/XXXXXXXXXXXX@developer.gserviceaccount.com","client_id":"XXXXXXXXXXXX.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}

View File

@@ -0,0 +1,20 @@
Bag Attributes
friendlyName: privatekey
localKeyID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
XXXXxyXXXXXXXxxyxxxX9y0XXYXXXXYXXxXyxxXxXxXXXyXXXXx4yx1xy1xyYxxY
1XxYy38YxXxxxyXxyyxx+xxxxyx1Y1xYx7yx2/Y1XyyXYYYxY5YXxX0xY/Y642yX
zYYxYXzXYxY0Y8y9YxyYXxxX40YyXxxXX4XXxx7XxXxxXyXxYYXxXyxX5XY0Yy2X
1YX0XXyy6YXyXx9XxXxyXX9XXYXxXxXXXXXXxYXYY3Y8Yy311XYYY81XyY14Xyyx
xXyx7xxXXXxxxxyyyX4YYYXyYyYXyxX4XYXYyxXYyx9xy23xXYyXyxYxXxx1XXXY
y98yX6yYxyyyX4Xyx1Xy/0yxxYxXxYYx2xx7yYXXXxYXXXxyXyyYYxx5XX2xxyxy
y6Yyyx0XX3YYYyx9YYXXXX7y0yxXXy+90XYz1y2xyx7yXxX+8X0xYxXXYxxyxYYy
YXx8Yy4yX0Xyxxx6yYX92yxy1YYYzyyyyxy55x/yyXXXYYXYXXzXXxYYxyXY8XXX
+y9+yXxX7XxxyYYxxXYxyY623xxXxYX59x5Y6yYyXYY4YxXXYXXXYxXYxXxXXx6x
YXX7XxXX2X0XY7YXyYy1XXxYXxXxYY1xXXxxxyy+07zXYxYxxXyyxxyxXx1XYy5X
5XYzyxYxXXYyX9XX7xX8xXxx+XXYyYXXXX5YY1x8Yxyx54Xy/1XXyyYXY5YxYyxY
XyyxXyX/YxxXXXxXXYXxyxx63xX/xxyYXXyYzx0XY+YxX5xyYyyxxxXXYX/94XXy
Xx63xYxXyXY3/XXxyyXX15XXXyz08XYY5YYXY/YXy/96x68XyyXXxYyXy4xYXx5x
7yxxyxxYxXxyx3y=
-----END PRIVATE KEY-----

View File

@@ -0,0 +1,114 @@
// Copyright 2011 The goauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This program makes a read only call to the Google Cloud Storage API,
// authenticated with OAuth2. A list of example APIs can be found at
// https://code.google.com/oauthplayground/
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"code.google.com/p/goauth2/oauth/jwt"
)
const scope = "https://www.googleapis.com/auth/devstorage.read_only"
var (
secretsFile = flag.String("s", "", "JSON encoded secrets for the service account")
pemFile = flag.String("k", "", "private pem key file for the service account")
)
const usageMsg = `
You must specify -k and -s.
To obtain client secrets and pem, see the "OAuth 2 Credentials" section under
the "API Access" tab on this page: https://code.google.com/apis/console/
Google Cloud Storage must also be turned on in the API console.
`
func main() {
flag.Parse()
if *secretsFile == "" || *pemFile == "" {
flag.Usage()
fmt.Println(usageMsg)
return
}
// Read the secret file bytes into the config.
secretBytes, err := ioutil.ReadFile(*secretsFile)
if err != nil {
log.Fatal("error reading secerets file:", err)
}
var config struct {
Web struct {
ClientEmail string `json:"client_email"`
ClientID string `json:"client_id"`
TokenURI string `json:"token_uri"`
}
}
err = json.Unmarshal(secretBytes, &config)
if err != nil {
log.Fatal("error unmarshalling secerets:", err)
}
// Get the project ID from the client ID.
projectID := strings.SplitN(config.Web.ClientID, "-", 2)[0]
// Read the pem file bytes for the private key.
keyBytes, err := ioutil.ReadFile(*pemFile)
if err != nil {
log.Fatal("error reading private key file:", err)
}
// Craft the ClaimSet and JWT token.
t := jwt.NewToken(config.Web.ClientEmail, scope, keyBytes)
t.ClaimSet.Aud = config.Web.TokenURI
// We need to provide a client.
c := &http.Client{}
// Get the access token.
o, err := t.Assert(c)
if err != nil {
log.Fatal("assertion error:", err)
}
// Refresh token will be missing, but this access_token will be good
// for one hour.
fmt.Printf("access_token = %v\n", o.AccessToken)
fmt.Printf("refresh_token = %v\n", o.RefreshToken)
fmt.Printf("expires %v\n", o.Expiry)
// Form the request to list Google Cloud Storage buckets.
req, err := http.NewRequest("GET", "https://storage.googleapis.com/", nil)
if err != nil {
log.Fatal("http.NewRequest:", err)
}
req.Header.Set("Authorization", "OAuth "+o.AccessToken)
req.Header.Set("x-goog-api-version", "2")
req.Header.Set("x-goog-project-id", projectID)
// Make the request.
r, err := c.Do(req)
if err != nil {
log.Fatal("API request error:", err)
}
defer r.Body.Close()
// Write the response to standard output.
res, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Fatal("error reading API request results:", err)
}
fmt.Printf("\nRESULT:\n%s\n", res)
}

View File

@@ -0,0 +1,511 @@
// Copyright 2012 The goauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The jwt package provides support for creating credentials for OAuth2 service
// account requests.
//
// For examples of the package usage please see jwt_test.go.
// Example usage (error handling omitted for brevity):
//
// // Craft the ClaimSet and JWT token.
// iss := "XXXXXXXXXXXX@developer.gserviceaccount.com"
// scope := "https://www.googleapis.com/auth/devstorage.read_only"
// t := jwt.NewToken(iss, scope, pemKeyBytes)
//
// // We need to provide a client.
// c := &http.Client{}
//
// // Get the access token.
// o, _ := t.Assert(c)
//
// // Form the request to the service.
// req, _ := http.NewRequest("GET", "https://storage.googleapis.com/", nil)
// req.Header.Set("Authorization", "OAuth "+o.AccessToken)
// req.Header.Set("x-goog-api-version", "2")
// req.Header.Set("x-goog-project-id", "XXXXXXXXXXXX")
//
// // Make the request.
// result, _ := c.Do(req)
//
// For info on OAuth2 service accounts please see the online documentation.
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount
//
package jwt
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"time"
"code.google.com/p/goauth2/oauth"
)
// These are the default/standard values for this to work for Google service accounts.
const (
stdAlgorithm = "RS256"
stdType = "JWT"
stdAssertionType = "http://oauth.net/grant_type/jwt/1.0/bearer"
stdGrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
stdAud = "https://accounts.google.com/o/oauth2/token"
)
var (
ErrInvalidKey = errors.New("Invalid Key")
)
// base64Encode returns and Base64url encoded version of the input string with any
// trailing "=" stripped.
func base64Encode(b []byte) string {
return strings.TrimRight(base64.URLEncoding.EncodeToString(b), "=")
}
// base64Decode decodes the Base64url encoded string
func base64Decode(s string) ([]byte, error) {
// add back missing padding
switch len(s) % 4 {
case 2:
s += "=="
case 3:
s += "="
}
return base64.URLEncoding.DecodeString(s)
}
// The JWT claim set contains information about the JWT including the
// permissions being requested (scopes), the target of the token, the issuer,
// the time the token was issued, and the lifetime of the token.
//
// Aud is usually https://accounts.google.com/o/oauth2/token
type ClaimSet struct {
Iss string `json:"iss"` // email address of the client_id of the application making the access token request
Scope string `json:"scope,omitempty"` // space-delimited list of the permissions the application requests
Aud string `json:"aud"` // descriptor of the intended target of the assertion (Optional).
Prn string `json:"prn,omitempty"` // email for which the application is requesting delegated access (Optional).
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
Typ string `json:"typ,omitempty"`
Sub string `json:"sub,omitempty"` // Add support for googleapi delegation support
// See http://tools.ietf.org/html/draft-jones-json-web-token-10#section-4.3
// This array is marshalled using custom code (see (c *ClaimSet) encode()).
PrivateClaims map[string]interface{} `json:"-"`
exp time.Time
iat time.Time
}
// setTimes sets iat and exp to time.Now() and iat.Add(time.Hour) respectively.
//
// Note that these times have nothing to do with the expiration time for the
// access_token returned by the server. These have to do with the lifetime of
// the encoded JWT.
//
// A JWT can be re-used for up to one hour after it was encoded. The access
// token that is granted will also be good for one hour so there is little point
// in trying to use the JWT a second time.
func (c *ClaimSet) setTimes(t time.Time) {
c.iat = t
c.exp = c.iat.Add(time.Hour)
}
var (
jsonStart = []byte{'{'}
jsonEnd = []byte{'}'}
)
// encode returns the Base64url encoded form of the Signature.
func (c *ClaimSet) encode() string {
if c.exp.IsZero() || c.iat.IsZero() {
c.setTimes(time.Now())
}
if c.Aud == "" {
c.Aud = stdAud
}
c.Exp = c.exp.Unix()
c.Iat = c.iat.Unix()
b, err := json.Marshal(c)
if err != nil {
panic(err)
}
if len(c.PrivateClaims) == 0 {
return base64Encode(b)
}
// Marshal private claim set and then append it to b.
prv, err := json.Marshal(c.PrivateClaims)
if err != nil {
panic(fmt.Errorf("Invalid map of private claims %v", c.PrivateClaims))
}
// Concatenate public and private claim JSON objects.
if !bytes.HasSuffix(b, jsonEnd) {
panic(fmt.Errorf("Invalid JSON %s", b))
}
if !bytes.HasPrefix(prv, jsonStart) {
panic(fmt.Errorf("Invalid JSON %s", prv))
}
b[len(b)-1] = ',' // Replace closing curly brace with a comma.
b = append(b, prv[1:]...) // Append private claims.
return base64Encode(b)
}
// Header describes the algorithm and type of token being generated,
// and optionally a KeyID describing additional parameters for the
// signature.
type Header struct {
Algorithm string `json:"alg"`
Type string `json:"typ"`
KeyId string `json:"kid,omitempty"`
}
func (h *Header) encode() string {
b, err := json.Marshal(h)
if err != nil {
panic(err)
}
return base64Encode(b)
}
// A JWT is composed of three parts: a header, a claim set, and a signature.
// The well formed and encoded JWT can then be exchanged for an access token.
//
// The Token is not a JWT, but is is encoded to produce a well formed JWT.
//
// When obtaining a key from the Google API console it will be downloaded in a
// PKCS12 encoding. To use this key you will need to convert it to a PEM file.
// This can be achieved with openssl.
//
// $ openssl pkcs12 -in <key.p12> -nocerts -passin pass:notasecret -nodes -out <key.pem>
//
// The contents of this file can then be used as the Key.
type Token struct {
ClaimSet *ClaimSet // claim set used to construct the JWT
Header *Header // header used to construct the JWT
Key []byte // PEM printable encoding of the private key
pKey *rsa.PrivateKey
header string
claim string
sig string
useExternalSigner bool
signer Signer
}
// NewToken returns a filled in *Token based on the standard header,
// and sets the Iat and Exp times based on when the call to Assert is
// made.
func NewToken(iss, scope string, key []byte) *Token {
c := &ClaimSet{
Iss: iss,
Scope: scope,
Aud: stdAud,
}
h := &Header{
Algorithm: stdAlgorithm,
Type: stdType,
}
t := &Token{
ClaimSet: c,
Header: h,
Key: key,
}
return t
}
// Signer is an interface that given a JWT token, returns the header &
// claim (serialized and urlEncoded to a byte slice), along with the
// signature and an error (if any occured). It could modify any data
// to sign (typically the KeyID).
//
// Example usage where a SHA256 hash of the original url-encoded token
// with an added KeyID and secret data is used as a signature:
//
// var privateData = "secret data added to hash, indexed by KeyID"
//
// type SigningService struct{}
//
// func (ss *SigningService) Sign(in *jwt.Token) (newTokenData, sig []byte, err error) {
// in.Header.KeyID = "signing service"
// newTokenData = in.EncodeWithoutSignature()
// dataToSign := fmt.Sprintf("%s.%s", newTokenData, privateData)
// h := sha256.New()
// _, err := h.Write([]byte(dataToSign))
// sig = h.Sum(nil)
// return
// }
type Signer interface {
Sign(in *Token) (tokenData, signature []byte, err error)
}
// NewSignerToken returns a *Token, using an external signer function
func NewSignerToken(iss, scope string, signer Signer) *Token {
t := NewToken(iss, scope, nil)
t.useExternalSigner = true
t.signer = signer
return t
}
// Expired returns a boolean value letting us know if the token has expired.
func (t *Token) Expired() bool {
return t.ClaimSet.exp.Before(time.Now())
}
// Encode constructs and signs a Token returning a JWT ready to use for
// requesting an access token.
func (t *Token) Encode() (string, error) {
var tok string
t.header = t.Header.encode()
t.claim = t.ClaimSet.encode()
err := t.sign()
if err != nil {
return tok, err
}
tok = fmt.Sprintf("%s.%s.%s", t.header, t.claim, t.sig)
return tok, nil
}
// EncodeWithoutSignature returns the url-encoded value of the Token
// before signing has occured (typically for use by external signers).
func (t *Token) EncodeWithoutSignature() string {
t.header = t.Header.encode()
t.claim = t.ClaimSet.encode()
return fmt.Sprintf("%s.%s", t.header, t.claim)
}
// sign computes the signature for a Token. The details for this can be found
// in the OAuth2 Service Account documentation.
// https://developers.google.com/accounts/docs/OAuth2ServiceAccount#computingsignature
func (t *Token) sign() error {
if t.useExternalSigner {
fulldata, sig, err := t.signer.Sign(t)
if err != nil {
return err
}
split := strings.Split(string(fulldata), ".")
if len(split) != 2 {
return errors.New("no token returned")
}
t.header = split[0]
t.claim = split[1]
t.sig = base64Encode(sig)
return err
}
ss := fmt.Sprintf("%s.%s", t.header, t.claim)
if t.pKey == nil {
err := t.parsePrivateKey()
if err != nil {
return err
}
}
h := sha256.New()
h.Write([]byte(ss))
b, err := rsa.SignPKCS1v15(rand.Reader, t.pKey, crypto.SHA256, h.Sum(nil))
t.sig = base64Encode(b)
return err
}
// parsePrivateKey converts the Token's Key ([]byte) into a parsed
// rsa.PrivateKey. If the key is not well formed this method will return an
// ErrInvalidKey error.
func (t *Token) parsePrivateKey() error {
block, _ := pem.Decode(t.Key)
if block == nil {
return ErrInvalidKey
}
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
parsedKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return err
}
}
var ok bool
t.pKey, ok = parsedKey.(*rsa.PrivateKey)
if !ok {
return ErrInvalidKey
}
return nil
}
// Assert obtains an *oauth.Token from the remote server by encoding and sending
// a JWT. The access_token will expire in one hour (3600 seconds) and cannot be
// refreshed (no refresh_token is returned with the response). Once this token
// expires call this method again to get a fresh one.
func (t *Token) Assert(c *http.Client) (*oauth.Token, error) {
var o *oauth.Token
t.ClaimSet.setTimes(time.Now())
u, v, err := t.buildRequest()
if err != nil {
return o, err
}
resp, err := c.PostForm(u, v)
if err != nil {
return o, err
}
o, err = handleResponse(resp)
return o, err
}
// buildRequest sets up the URL values and the proper URL string for making our
// access_token request.
func (t *Token) buildRequest() (string, url.Values, error) {
v := url.Values{}
j, err := t.Encode()
if err != nil {
return t.ClaimSet.Aud, v, err
}
v.Set("grant_type", stdGrantType)
v.Set("assertion", j)
return t.ClaimSet.Aud, v, nil
}
// Used for decoding the response body.
type respBody struct {
IdToken string `json:"id_token"`
Access string `json:"access_token"`
Type string `json:"token_type"`
ExpiresIn time.Duration `json:"expires_in"`
}
// handleResponse returns a filled in *oauth.Token given the *http.Response from
// a *http.Request created by buildRequest.
func handleResponse(r *http.Response) (*oauth.Token, error) {
o := &oauth.Token{}
defer r.Body.Close()
if r.StatusCode != 200 {
return o, errors.New("invalid response: " + r.Status)
}
b := &respBody{}
err := json.NewDecoder(r.Body).Decode(b)
if err != nil {
return o, err
}
o.AccessToken = b.Access
if b.IdToken != "" {
// decode returned id token to get expiry
o.AccessToken = b.IdToken
s := strings.Split(b.IdToken, ".")
if len(s) < 2 {
return nil, errors.New("invalid token received")
}
d, err := base64Decode(s[1])
if err != nil {
return o, err
}
c := &ClaimSet{}
err = json.NewDecoder(bytes.NewBuffer(d)).Decode(c)
if err != nil {
return o, err
}
o.Expiry = time.Unix(c.Exp, 0)
return o, nil
}
o.Expiry = time.Now().Add(b.ExpiresIn * time.Second)
return o, nil
}
// Transport implements http.RoundTripper. When configured with a valid
// JWT and OAuth tokens it can be used to make authenticated HTTP requests.
//
// t := &jwt.Transport{jwtToken, oauthToken}
// r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
//
// It will automatically refresh the OAuth token if it can, updating in place.
type Transport struct {
JWTToken *Token
OAuthToken *oauth.Token
// Transport is the HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}
// Creates a new authenticated transport.
func NewTransport(token *Token) (*Transport, error) {
oa, err := token.Assert(new(http.Client))
if err != nil {
return nil, err
}
return &Transport{
JWTToken: token,
OAuthToken: oa,
}, nil
}
// Client returns an *http.Client that makes OAuth-authenticated requests.
func (t *Transport) Client() *http.Client {
return &http.Client{Transport: t}
}
// Fetches the internal transport.
func (t *Transport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// RoundTrip executes a single HTTP transaction using the Transport's
// OAuthToken as authorization headers.
//
// This method will attempt to renew the token if it has expired and may return
// an error related to that token renewal before attempting the client request.
// If the token cannot be renewed a non-nil os.Error value will be returned.
// If the token is invalid callers should expect HTTP-level errors,
// as indicated by the Response's StatusCode.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
// Sanity check the two tokens
if t.JWTToken == nil {
return nil, fmt.Errorf("no JWT token supplied")
}
if t.OAuthToken == nil {
return nil, fmt.Errorf("no OAuth token supplied")
}
// Refresh the OAuth token if it has expired
if t.OAuthToken.Expired() {
if oa, err := t.JWTToken.Assert(new(http.Client)); err != nil {
return nil, err
} else {
t.OAuthToken = oa
}
}
// To set the Authorization header, we must make a copy of the Request
// so that we don't modify the Request we were given.
// This is required by the specification of http.RoundTripper.
req = cloneRequest(req)
req.Header.Set("Authorization", "Bearer "+t.OAuthToken.AccessToken)
// Make the HTTP request.
return t.transport().RoundTrip(req)
}
// 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)
for k, s := range r.Header {
r2.Header[k] = s
}
return r2
}

View File

@@ -0,0 +1,486 @@
// Copyright 2012 The goauth2 Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// For package documentation please see jwt.go.
//
package jwt
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/json"
"encoding/pem"
"io/ioutil"
"net/http"
"testing"
"time"
)
const (
stdHeaderStr = `{"alg":"RS256","typ":"JWT"}`
iss = "761326798069-r5mljlln1rd4lrbhg75efgigp36m78j5@developer.gserviceaccount.com"
scope = "https://www.googleapis.com/auth/prediction"
exp = 1328554385
iat = 1328550785 // exp + 1 hour
)
// Base64url encoded Header
const headerEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
// Base64url encoded ClaimSet
const claimSetEnc = "eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ"
// Base64url encoded Signature
const sigEnc = "olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
// Base64url encoded Token
const tokEnc = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI3NjEzMjY3OTgwNjktcjVtbGpsbG4xcmQ0bHJiaGc3NWVmZ2lncDM2bTc4ajVAZGV2ZWxvcGVyLmdzZXJ2aWNlYWNjb3VudC5jb20iLCJzY29wZSI6Imh0dHBzOi8vd3d3Lmdvb2dsZWFwaXMuY29tL2F1dGgvcHJlZGljdGlvbiIsImF1ZCI6Imh0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbS9vL29hdXRoMi90b2tlbiIsImV4cCI6MTMyODU1NDM4NSwiaWF0IjoxMzI4NTUwNzg1fQ.olukbHreNiYrgiGCTEmY3eWGeTvYDSUHYoE84Jz3BRPBSaMdZMNOn_0CYK7UHPO7OdvUofjwft1dH59UxE9GWS02pjFti1uAQoImaqjLZoTXr8qiF6O_kDa9JNoykklWlRAIwGIZkDupCS-8cTAnM_ksSymiH1coKJrLDUX_BM0x2f4iMFQzhL5vT1ll-ZipJ0lNlxb5QsyXxDYcxtHYguF12-vpv3ItgT0STfcXoWzIGQoEbhwB9SBp9JYcQ8Ygz6pYDjm0rWX9LrchmTyDArCodpKLFtutNgcIFUP9fWxvwd1C2dNw5GjLcKr9a_SAERyoJ2WnCR1_j9N0wD2o0g"
// Private key for testing
const privateKeyPem = `-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj
7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/
xmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs
SliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18
pe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk
SBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk
nQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq
HD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y
nHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9
IisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2
YCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU
Z422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ
vzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP
B8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl
aLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2
eCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI
aqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk
klORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ
CFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu
UqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg
soBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28
bvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH
504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL
YXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx
BeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==
-----END RSA PRIVATE KEY-----`
// Public key to go with the private key for testing
const publicKeyPem = `-----BEGIN CERTIFICATE-----
MIIDIzCCAgugAwIBAgIJAMfISuBQ5m+5MA0GCSqGSIb3DQEBBQUAMBUxEzARBgNV
BAMTCnVuaXQtdGVzdHMwHhcNMTExMjA2MTYyNjAyWhcNMjExMjAzMTYyNjAyWjAV
MRMwEQYDVQQDEwp1bml0LXRlc3RzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj7wZgkdmM
7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/xmVU1Wer
uQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYsSliS5qQp
gyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18pe+zpyl4
+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xkSBc//fy3
ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABo3YwdDAdBgNVHQ4EFgQU2RQ8yO+O
gN8oVW2SW7RLrfYd9jEwRQYDVR0jBD4wPIAU2RQ8yO+OgN8oVW2SW7RLrfYd9jGh
GaQXMBUxEzARBgNVBAMTCnVuaXQtdGVzdHOCCQDHyErgUOZvuTAMBgNVHRMEBTAD
AQH/MA0GCSqGSIb3DQEBBQUAA4IBAQBRv+M/6+FiVu7KXNjFI5pSN17OcW5QUtPr
odJMlWrJBtynn/TA1oJlYu3yV5clc/71Vr/AxuX5xGP+IXL32YDF9lTUJXG/uUGk
+JETpKmQviPbRsvzYhz4pf6ZIOZMc3/GIcNq92ECbseGO+yAgyWUVKMmZM0HqXC9
ovNslqe0M8C1sLm1zAR5z/h/litE7/8O2ietija3Q/qtl2TOXJdCA6sgjJX2WUql
ybrC55ct18NKf3qhpcEkGQvFU40rVYApJpi98DiZPYFdx1oBDp/f4uZ3ojpxRVFT
cDwcJLfNRCPUhormsY7fDS9xSyThiHsW9mjJYdcaKQkwYZ0F11yB
-----END CERTIFICATE-----`
var (
privateKeyPemBytes = []byte(privateKeyPem)
publicKeyPemBytes = []byte(publicKeyPem)
stdHeader = &Header{Algorithm: stdAlgorithm, Type: stdType}
)
// Testing the urlEncode function.
func TestUrlEncode(t *testing.T) {
enc := base64Encode([]byte(stdHeaderStr))
b := []byte(enc)
if b[len(b)-1] == 61 {
t.Error("TestUrlEncode: last chat == \"=\"")
}
if enc != headerEnc {
t.Error("TestUrlEncode: enc != headerEnc")
t.Errorf(" enc = %s", enc)
t.Errorf(" headerEnc = %s", headerEnc)
}
}
// Test that the times are set properly.
func TestClaimSetSetTimes(t *testing.T) {
c := &ClaimSet{
Iss: iss,
Scope: scope,
}
iat := time.Unix(iat, 0)
c.setTimes(iat)
if c.exp.Unix() != exp {
t.Error("TestClaimSetSetTimes: c.exp != exp")
t.Errorf(" c.Exp = %d", c.exp.Unix())
t.Errorf(" exp = %d", exp)
}
}
// Given a well formed ClaimSet, test for proper encoding.
func TestClaimSetEncode(t *testing.T) {
c := &ClaimSet{
Iss: iss,
Scope: scope,
exp: time.Unix(exp, 0),
iat: time.Unix(iat, 0),
}
enc := c.encode()
re, err := base64Decode(enc)
if err != nil {
t.Fatalf("error decoding encoded claim set: %v", err)
}
wa, err := base64Decode(claimSetEnc)
if err != nil {
t.Fatalf("error decoding encoded expected claim set: %v", err)
}
if enc != claimSetEnc {
t.Error("TestClaimSetEncode: enc != claimSetEnc")
t.Errorf(" enc = %s", string(re))
t.Errorf(" claimSetEnc = %s", string(wa))
}
}
// Test that claim sets with private claim names are encoded correctly.
func TestClaimSetWithPrivateNameEncode(t *testing.T) {
iatT := time.Unix(iat, 0)
expT := time.Unix(exp, 0)
i, err := json.Marshal(iatT.Unix())
if err != nil {
t.Fatalf("error marshaling iatT value of %v: %v", iatT.Unix(), err)
}
iatStr := string(i)
e, err := json.Marshal(expT.Unix())
if err != nil {
t.Fatalf("error marshaling expT value of %v: %v", expT.Unix(), err)
}
expStr := string(e)
testCases := []struct {
desc string
input map[string]interface{}
want string
}{
// Test a simple int field.
{
"single simple field",
map[string]interface{}{"amount": 22},
`{` +
`"iss":"` + iss + `",` +
`"scope":"` + scope + `",` +
`"aud":"` + stdAud + `",` +
`"exp":` + expStr + `,` +
`"iat":` + iatStr + `,` +
`"amount":22` +
`}`,
},
{
"multiple simple fields",
map[string]interface{}{"tracking_code": "axZf", "amount": 22},
`{` +
`"iss":"` + iss + `",` +
`"scope":"` + scope + `",` +
`"aud":"` + stdAud + `",` +
`"exp":` + expStr + `,` +
`"iat":` + iatStr + `,` +
`"amount":22,` +
`"tracking_code":"axZf"` +
`}`,
},
{
"nested struct fields",
map[string]interface{}{
"tracking_code": "axZf",
"purchase": struct {
Description string `json:"desc"`
Quantity int32 `json:"q"`
Time int64 `json:"t"`
}{
"toaster",
5,
iat,
},
},
`{` +
`"iss":"` + iss + `",` +
`"scope":"` + scope + `",` +
`"aud":"` + stdAud + `",` +
`"exp":` + expStr + `,` +
`"iat":` + iatStr + `,` +
`"purchase":{"desc":"toaster","q":5,"t":` + iatStr + `},` +
`"tracking_code":"axZf"` +
`}`,
},
}
for _, testCase := range testCases {
c := &ClaimSet{
Iss: iss,
Scope: scope,
Aud: stdAud,
iat: iatT,
exp: expT,
PrivateClaims: testCase.input,
}
cJSON, err := base64Decode(c.encode())
if err != nil {
t.Fatalf("error decoding claim set: %v", err)
}
if string(cJSON) != testCase.want {
t.Errorf("TestClaimSetWithPrivateNameEncode: enc != want in case %s", testCase.desc)
t.Errorf(" enc = %s", cJSON)
t.Errorf(" want = %s", testCase.want)
}
}
}
// Test the NewToken constructor.
func TestNewToken(t *testing.T) {
tok := NewToken(iss, scope, privateKeyPemBytes)
if tok.ClaimSet.Iss != iss {
t.Error("TestNewToken: tok.ClaimSet.Iss != iss")
t.Errorf(" tok.ClaimSet.Iss = %s", tok.ClaimSet.Iss)
t.Errorf(" iss = %s", iss)
}
if tok.ClaimSet.Scope != scope {
t.Error("TestNewToken: tok.ClaimSet.Scope != scope")
t.Errorf(" tok.ClaimSet.Scope = %s", tok.ClaimSet.Scope)
t.Errorf(" scope = %s", scope)
}
if tok.ClaimSet.Aud != stdAud {
t.Error("TestNewToken: tok.ClaimSet.Aud != stdAud")
t.Errorf(" tok.ClaimSet.Aud = %s", tok.ClaimSet.Aud)
t.Errorf(" stdAud = %s", stdAud)
}
if !bytes.Equal(tok.Key, privateKeyPemBytes) {
t.Error("TestNewToken: tok.Key != privateKeyPemBytes")
t.Errorf(" tok.Key = %s", tok.Key)
t.Errorf(" privateKeyPemBytes = %s", privateKeyPemBytes)
}
}
// Make sure the private key parsing functions work.
func TestParsePrivateKey(t *testing.T) {
tok := &Token{
Key: privateKeyPemBytes,
}
err := tok.parsePrivateKey()
if err != nil {
t.Errorf("TestParsePrivateKey:tok.parsePrivateKey: %v", err)
}
}
// Test that the token signature generated matches the golden standard.
func TestTokenSign(t *testing.T) {
tok := &Token{
Key: privateKeyPemBytes,
claim: claimSetEnc,
header: headerEnc,
}
err := tok.parsePrivateKey()
if err != nil {
t.Errorf("TestTokenSign:tok.parsePrivateKey: %v", err)
}
err = tok.sign()
if err != nil {
t.Errorf("TestTokenSign:tok.sign: %v", err)
}
if tok.sig != sigEnc {
t.Error("TestTokenSign: tok.sig != sigEnc")
t.Errorf(" tok.sig = %s", tok.sig)
t.Errorf(" sigEnc = %s", sigEnc)
}
}
// Test that the token expiration function is working.
func TestTokenExpired(t *testing.T) {
c := &ClaimSet{}
tok := &Token{
ClaimSet: c,
}
now := time.Now()
c.setTimes(now)
if tok.Expired() != false {
t.Error("TestTokenExpired: tok.Expired != false")
}
// Set the times as if they were set 2 hours ago.
c.setTimes(now.Add(-2 * time.Hour))
if tok.Expired() != true {
t.Error("TestTokenExpired: tok.Expired != true")
}
}
// Given a well formed Token, test for proper encoding.
func TestTokenEncode(t *testing.T) {
c := &ClaimSet{
Iss: iss,
Scope: scope,
exp: time.Unix(exp, 0),
iat: time.Unix(iat, 0),
}
tok := &Token{
ClaimSet: c,
Header: stdHeader,
Key: privateKeyPemBytes,
}
enc, err := tok.Encode()
if err != nil {
t.Errorf("TestTokenEncode:tok.Assertion: %v", err)
}
if enc != tokEnc {
t.Error("TestTokenEncode: enc != tokEnc")
t.Errorf(" enc = %s", enc)
t.Errorf(" tokEnc = %s", tokEnc)
}
}
// Given a well formed Token we should get back a well formed request.
func TestBuildRequest(t *testing.T) {
c := &ClaimSet{
Iss: iss,
Scope: scope,
exp: time.Unix(exp, 0),
iat: time.Unix(iat, 0),
}
tok := &Token{
ClaimSet: c,
Header: stdHeader,
Key: privateKeyPemBytes,
}
u, v, err := tok.buildRequest()
if err != nil {
t.Errorf("TestBuildRequest:BuildRequest: %v", err)
}
if u != c.Aud {
t.Error("TestBuildRequest: u != c.Aud")
t.Errorf(" u = %s", u)
t.Errorf(" c.Aud = %s", c.Aud)
}
if v.Get("grant_type") != stdGrantType {
t.Error("TestBuildRequest: grant_type != stdGrantType")
t.Errorf(" grant_type = %s", v.Get("grant_type"))
t.Errorf(" stdGrantType = %s", stdGrantType)
}
if v.Get("assertion") != tokEnc {
t.Error("TestBuildRequest: assertion != tokEnc")
t.Errorf(" assertion = %s", v.Get("assertion"))
t.Errorf(" tokEnc = %s", tokEnc)
}
}
// Given a well formed access request response we should get back a oauth.Token.
func TestHandleResponse(t *testing.T) {
rb := &respBody{
Access: "1/8xbJqaOZXSUZbHLl5EOtu1pxz3fmmetKx9W8CV4t79M",
Type: "Bearer",
ExpiresIn: 3600,
}
b, err := json.Marshal(rb)
if err != nil {
t.Errorf("TestHandleResponse:json.Marshal: %v", err)
}
r := &http.Response{
Status: "200 OK",
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}
o, err := handleResponse(r)
if err != nil {
t.Errorf("TestHandleResponse:handleResponse: %v", err)
}
if o.AccessToken != rb.Access {
t.Error("TestHandleResponse: o.AccessToken != rb.Access")
t.Errorf(" o.AccessToken = %s", o.AccessToken)
t.Errorf(" rb.Access = %s", rb.Access)
}
if o.Expired() {
t.Error("TestHandleResponse: o.Expired == true")
}
}
// passthrough signature for test
type FakeSigner struct{}
func (f FakeSigner) Sign(tok *Token) ([]byte, []byte, error) {
block, _ := pem.Decode(privateKeyPemBytes)
pKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
ss := headerEnc + "." + claimSetEnc
h := sha256.New()
h.Write([]byte(ss))
b, _ := rsa.SignPKCS1v15(rand.Reader, pKey, crypto.SHA256, h.Sum(nil))
return []byte(ss), b, nil
}
// Given an external signer, get back a valid and signed JWT
func TestExternalSigner(t *testing.T) {
tok := NewSignerToken(iss, scope, FakeSigner{})
enc, _ := tok.Encode()
if enc != tokEnc {
t.Errorf("TestExternalSigner: enc != tokEnc")
t.Errorf(" enc = %s", enc)
t.Errorf(" tokEnc = %s", tokEnc)
}
}
func TestHandleResponseWithNewExpiry(t *testing.T) {
rb := &respBody{
IdToken: tokEnc,
}
b, err := json.Marshal(rb)
if err != nil {
t.Errorf("TestHandleResponse:json.Marshal: %v", err)
}
r := &http.Response{
Status: "200 OK",
StatusCode: 200,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}
o, err := handleResponse(r)
if err != nil {
t.Errorf("TestHandleResponse:handleResponse: %v", err)
}
if o.Expiry != time.Unix(exp, 0) {
t.Error("TestHandleResponse: o.Expiry != exp")
t.Errorf(" o.Expiry = %s", o.Expiry)
t.Errorf(" exp = %s", time.Unix(exp, 0))
}
}
// Placeholder for future Assert tests.
func TestAssert(t *testing.T) {
// Since this method makes a call to BuildRequest, an htttp.Client, and
// finally HandleResponse there is not much more to test. This is here
// as a placeholder if that changes.
}
// Benchmark for the end-to-end encoding of a well formed token.
func BenchmarkTokenEncode(b *testing.B) {
b.StopTimer()
c := &ClaimSet{
Iss: iss,
Scope: scope,
exp: time.Unix(exp, 0),
iat: time.Unix(iat, 0),
}
tok := &Token{
ClaimSet: c,
Key: privateKeyPemBytes,
}
b.StartTimer()
for i := 0; i < b.N; i++ {
tok.Encode()
}
}

View File

@@ -0,0 +1,476 @@
// Copyright 2011 The goauth2 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 oauth supports making OAuth2-authenticated HTTP requests.
//
// Example usage:
//
// // Specify your configuration. (typically as a global variable)
// var config = &oauth.Config{
// ClientId: YOUR_CLIENT_ID,
// ClientSecret: YOUR_CLIENT_SECRET,
// Scope: "https://www.googleapis.com/auth/buzz",
// AuthURL: "https://accounts.google.com/o/oauth2/auth",
// TokenURL: "https://accounts.google.com/o/oauth2/token",
// RedirectURL: "http://you.example.org/handler",
// }
//
// // A landing page redirects to the OAuth provider to get the auth code.
// func landing(w http.ResponseWriter, r *http.Request) {
// http.Redirect(w, r, config.AuthCodeURL("foo"), http.StatusFound)
// }
//
// // The user will be redirected back to this handler, that takes the
// // "code" query parameter and Exchanges it for an access token.
// func handler(w http.ResponseWriter, r *http.Request) {
// t := &oauth.Transport{Config: config}
// t.Exchange(r.FormValue("code"))
// // The Transport now has a valid Token. Create an *http.Client
// // with which we can make authenticated API requests.
// c := t.Client()
// c.Post(...)
// // ...
// // btw, r.FormValue("state") == "foo"
// }
//
package oauth
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
)
// OAuthError is the error type returned by many operations.
//
// In retrospect it should not exist. Don't depend on it.
type OAuthError struct {
prefix string
msg string
}
func (oe OAuthError) Error() string {
return "OAuthError: " + oe.prefix + ": " + oe.msg
}
// Cache specifies the methods that implement a Token cache.
type Cache interface {
Token() (*Token, error)
PutToken(*Token) error
}
// CacheFile implements Cache. Its value is the name of the file in which
// the Token is stored in JSON format.
type CacheFile string
func (f CacheFile) Token() (*Token, error) {
file, err := os.Open(string(f))
if err != nil {
return nil, OAuthError{"CacheFile.Token", err.Error()}
}
defer file.Close()
tok := &Token{}
if err := json.NewDecoder(file).Decode(tok); err != nil {
return nil, OAuthError{"CacheFile.Token", err.Error()}
}
return tok, nil
}
func (f CacheFile) PutToken(tok *Token) error {
file, err := os.OpenFile(string(f), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
return OAuthError{"CacheFile.PutToken", err.Error()}
}
if err := json.NewEncoder(file).Encode(tok); err != nil {
file.Close()
return OAuthError{"CacheFile.PutToken", err.Error()}
}
if err := file.Close(); err != nil {
return OAuthError{"CacheFile.PutToken", err.Error()}
}
return nil
}
// Config is the configuration of an OAuth consumer.
type Config struct {
// ClientId is the OAuth client identifier used when communicating with
// the configured OAuth provider.
ClientId string
// ClientSecret is the OAuth client secret used when communicating with
// the configured OAuth provider.
ClientSecret string
// Scope identifies the level of access being requested. Multiple scope
// values should be provided as a space-delimited string.
Scope string
// AuthURL is the URL the user will be directed to in order to grant
// access.
AuthURL string
// TokenURL is the URL used to retrieve OAuth tokens.
TokenURL string
// RedirectURL is the URL to which the user will be returned after
// granting (or denying) access.
RedirectURL string
// TokenCache allows tokens to be cached for subsequent requests.
TokenCache Cache
// AccessType is an OAuth extension that gets sent as the
// "access_type" field in the URL from AuthCodeURL.
// See https://developers.google.com/accounts/docs/OAuth2WebServer.
// It may be "online" (the default) or "offline".
// If your application needs to refresh access tokens when the
// user is not present at the browser, then use offline. This
// will result in your application obtaining a refresh token
// the first time your application exchanges an authorization
// code for a user.
AccessType string
// ApprovalPrompt indicates whether the user should be
// re-prompted for consent. If set to "auto" (default) the
// user will be prompted only if they haven't previously
// granted consent and the code can only be exchanged for an
// access token.
// If set to "force" the user will always be prompted, and the
// code can be exchanged for a refresh token.
ApprovalPrompt string
}
// Token contains an end-user's tokens.
// This is the data you must store to persist authentication.
type Token struct {
AccessToken string
RefreshToken string
Expiry time.Time // If zero the token has no (known) expiry time.
// Extra optionally contains extra metadata from the server
// when updating a token. The only current key that may be
// populated is "id_token". It may be nil and will be
// initialized as needed.
Extra map[string]string
}
// Expired reports whether the token has expired or is invalid.
func (t *Token) Expired() bool {
if t.AccessToken == "" {
return true
}
if t.Expiry.IsZero() {
return false
}
return t.Expiry.Before(time.Now())
}
// Transport implements http.RoundTripper. When configured with a valid
// Config and Token it can be used to make authenticated HTTP requests.
//
// t := &oauth.Transport{config}
// t.Exchange(code)
// // t now contains a valid Token
// r, _, err := t.Client().Get("http://example.org/url/requiring/auth")
//
// It will automatically refresh the Token if it can,
// updating the supplied Token in place.
type Transport struct {
*Config
*Token
// mu guards modifying the token.
mu sync.Mutex
// Transport is the HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
// (It should never be an oauth.Transport.)
Transport http.RoundTripper
}
// Client returns an *http.Client that makes OAuth-authenticated requests.
func (t *Transport) Client() *http.Client {
return &http.Client{Transport: t}
}
func (t *Transport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}
// AuthCodeURL returns a URL that the end-user should be redirected to,
// so that they may obtain an authorization code.
func (c *Config) AuthCodeURL(state string) string {
url_, err := url.Parse(c.AuthURL)
if err != nil {
panic("AuthURL malformed: " + err.Error())
}
q := url.Values{
"response_type": {"code"},
"client_id": {c.ClientId},
"state": condVal(state),
"scope": condVal(c.Scope),
"redirect_uri": condVal(c.RedirectURL),
"access_type": condVal(c.AccessType),
"approval_prompt": condVal(c.ApprovalPrompt),
}.Encode()
if url_.RawQuery == "" {
url_.RawQuery = q
} else {
url_.RawQuery += "&" + q
}
return url_.String()
}
func condVal(v string) []string {
if v == "" {
return nil
}
return []string{v}
}
// Exchange takes a code and gets access Token from the remote server.
func (t *Transport) Exchange(code string) (*Token, error) {
if t.Config == nil {
return nil, OAuthError{"Exchange", "no Config supplied"}
}
// If the transport or the cache already has a token, it is
// passed to `updateToken` to preserve existing refresh token.
tok := t.Token
if tok == nil && t.TokenCache != nil {
tok, _ = t.TokenCache.Token()
}
if tok == nil {
tok = new(Token)
}
err := t.updateToken(tok, url.Values{
"grant_type": {"authorization_code"},
"redirect_uri": {t.RedirectURL},
"scope": {t.Scope},
"code": {code},
})
if err != nil {
return nil, err
}
t.Token = tok
if t.TokenCache != nil {
return tok, t.TokenCache.PutToken(tok)
}
return tok, nil
}
// RoundTrip executes a single HTTP transaction using the Transport's
// Token as authorization headers.
//
// This method will attempt to renew the Token if it has expired and may return
// an error related to that Token renewal before attempting the client request.
// If the Token cannot be renewed a non-nil os.Error value will be returned.
// If the Token is invalid callers should expect HTTP-level errors,
// as indicated by the Response's StatusCode.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
accessToken, err := t.getAccessToken()
if err != nil {
return nil, err
}
// To set the Authorization header, we must make a copy of the Request
// so that we don't modify the Request we were given.
// This is required by the specification of http.RoundTripper.
req = cloneRequest(req)
req.Header.Set("Authorization", "Bearer "+accessToken)
// Make the HTTP request.
return t.transport().RoundTrip(req)
}
func (t *Transport) getAccessToken() (string, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.Token == nil {
if t.Config == nil {
return "", OAuthError{"RoundTrip", "no Config supplied"}
}
if t.TokenCache == nil {
return "", OAuthError{"RoundTrip", "no Token supplied"}
}
var err error
t.Token, err = t.TokenCache.Token()
if err != nil {
return "", err
}
}
// Refresh the Token if it has expired.
if t.Expired() {
if err := t.Refresh(); err != nil {
return "", err
}
}
if t.AccessToken == "" {
return "", errors.New("no access token obtained from refresh")
}
return t.AccessToken, nil
}
// 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)
for k, s := range r.Header {
r2.Header[k] = s
}
return r2
}
// Refresh renews the Transport's AccessToken using its RefreshToken.
func (t *Transport) Refresh() error {
if t.Token == nil {
return OAuthError{"Refresh", "no existing Token"}
}
if t.RefreshToken == "" {
return OAuthError{"Refresh", "Token expired; no Refresh Token"}
}
if t.Config == nil {
return OAuthError{"Refresh", "no Config supplied"}
}
err := t.updateToken(t.Token, url.Values{
"grant_type": {"refresh_token"},
"refresh_token": {t.RefreshToken},
})
if err != nil {
return err
}
if t.TokenCache != nil {
return t.TokenCache.PutToken(t.Token)
}
return nil
}
// AuthenticateClient gets an access Token using the client_credentials grant
// type.
func (t *Transport) AuthenticateClient() error {
if t.Config == nil {
return OAuthError{"Exchange", "no Config supplied"}
}
if t.Token == nil {
t.Token = &Token{}
}
return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}})
}
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
// implements the OAuth2 spec correctly
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
// In summary:
// - Reddit only accepts client secret in the Authorization header
// - Dropbox accepts either it in URL param or Auth header, but not both.
// - Google only accepts URL param (not spec compliant?), not Auth header
func providerAuthHeaderWorks(tokenURL string) bool {
if strings.HasPrefix(tokenURL, "https://accounts.google.com/") ||
strings.HasPrefix(tokenURL, "https://github.com/") ||
strings.HasPrefix(tokenURL, "https://api.instagram.com/") ||
strings.HasPrefix(tokenURL, "https://www.douban.com/") {
// Some sites fail to implement the OAuth2 spec fully.
return false
}
// Assume the provider implements the spec properly
// otherwise. We can add more exceptions as they're
// discovered. We will _not_ be adding configurable hooks
// to this package to let users select server bugs.
return true
}
// updateToken mutates both tok and v.
func (t *Transport) updateToken(tok *Token, v url.Values) error {
v.Set("client_id", t.ClientId)
bustedAuth := !providerAuthHeaderWorks(t.TokenURL)
if bustedAuth {
v.Set("client_secret", t.ClientSecret)
}
client := &http.Client{Transport: t.transport()}
req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if !bustedAuth {
req.SetBasicAuth(t.ClientId, t.ClientSecret)
}
r, err := client.Do(req)
if err != nil {
return err
}
defer r.Body.Close()
if r.StatusCode != 200 {
return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status}
}
var b struct {
Access string `json:"access_token"`
Refresh string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` // seconds
Id string `json:"id_token"`
}
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return err
}
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content {
case "application/x-www-form-urlencoded", "text/plain":
vals, err := url.ParseQuery(string(body))
if err != nil {
return err
}
b.Access = vals.Get("access_token")
b.Refresh = vals.Get("refresh_token")
b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64)
b.Id = vals.Get("id_token")
default:
if err = json.Unmarshal(body, &b); err != nil {
return fmt.Errorf("got bad response from server: %q", body)
}
}
if b.Access == "" {
return errors.New("received empty access token from authorization server")
}
tok.AccessToken = b.Access
// Don't overwrite `RefreshToken` with an empty value
if b.Refresh != "" {
tok.RefreshToken = b.Refresh
}
if b.ExpiresIn == 0 {
tok.Expiry = time.Time{}
} else {
tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second)
}
if b.Id != "" {
if tok.Extra == nil {
tok.Extra = make(map[string]string)
}
tok.Extra["id_token"] = b.Id
}
return nil
}

View File

@@ -0,0 +1,236 @@
// Copyright 2011 The goauth2 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 oauth
import (
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"runtime"
"testing"
"time"
)
var requests = []struct {
path, query, auth string // request
contenttype, body string // response
}{
{
path: "/token",
query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d",
contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: `
{
"access_token":"token1",
"refresh_token":"refreshtoken1",
"id_token":"idtoken1",
"expires_in":3600
}
`,
},
{path: "/secure", auth: "Bearer token1", body: "first payload"},
{
path: "/token",
query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d",
contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: `
{
"access_token":"token2",
"refresh_token":"refreshtoken2",
"id_token":"idtoken2",
"expires_in":3600
}
`,
},
{path: "/secure", auth: "Bearer token2", body: "second payload"},
{
path: "/token",
query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d",
contenttype: "application/x-www-form-urlencoded",
body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
},
{path: "/secure", auth: "Bearer token3", body: "third payload"},
{
path: "/token",
query: "grant_type=client_credentials&client_id=cl13nt1d",
contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: `
{
"access_token":"token4",
"expires_in":3600
}
`,
},
{path: "/secure", auth: "Bearer token4", body: "fourth payload"},
}
func TestOAuth(t *testing.T) {
// Set up test server.
n := 0
handler := func(w http.ResponseWriter, r *http.Request) {
if n >= len(requests) {
t.Errorf("too many requests: %d", n)
return
}
req := requests[n]
n++
// Check request.
if g, w := r.URL.Path, req.path; g != w {
t.Errorf("request[%d] got path %s, want %s", n, g, w)
}
want, _ := url.ParseQuery(req.query)
for k := range want {
if g, w := r.FormValue(k), want.Get(k); g != w {
t.Errorf("query[%s] = %s, want %s", k, g, w)
}
}
if g, w := r.Header.Get("Authorization"), req.auth; w != "" && g != w {
t.Errorf("Authorization: %v, want %v", g, w)
}
// Send response.
w.Header().Set("Content-Type", req.contenttype)
io.WriteString(w, req.body)
}
server := httptest.NewServer(http.HandlerFunc(handler))
defer server.Close()
config := &Config{
ClientId: "cl13nt1d",
ClientSecret: "s3cr3t",
Scope: "https://example.net/scope",
AuthURL: server.URL + "/auth",
TokenURL: server.URL + "/token",
}
// TODO(adg): test AuthCodeURL
transport := &Transport{Config: config}
_, err := transport.Exchange("c0d3")
if err != nil {
t.Fatalf("Exchange: %v", err)
}
checkToken(t, transport.Token, "token1", "refreshtoken1", "idtoken1")
c := transport.Client()
resp, err := c.Get(server.URL + "/secure")
if err != nil {
t.Fatalf("Get: %v", err)
}
checkBody(t, resp, "first payload")
// test automatic refresh
transport.Expiry = time.Now().Add(-time.Hour)
resp, err = c.Get(server.URL + "/secure")
if err != nil {
t.Fatalf("Get: %v", err)
}
checkBody(t, resp, "second payload")
checkToken(t, transport.Token, "token2", "refreshtoken2", "idtoken2")
// refresh one more time, but get URL-encoded token instead of JSON
transport.Expiry = time.Now().Add(-time.Hour)
resp, err = c.Get(server.URL + "/secure")
if err != nil {
t.Fatalf("Get: %v", err)
}
checkBody(t, resp, "third payload")
checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3")
transport.Token = &Token{}
err = transport.AuthenticateClient()
if err != nil {
t.Fatalf("AuthenticateClient: %v", err)
}
checkToken(t, transport.Token, "token4", "", "")
resp, err = c.Get(server.URL + "/secure")
if err != nil {
t.Fatalf("Get: %v", err)
}
checkBody(t, resp, "fourth payload")
}
func checkToken(t *testing.T, tok *Token, access, refresh, id string) {
if g, w := tok.AccessToken, access; g != w {
t.Errorf("AccessToken = %q, want %q", g, w)
}
if g, w := tok.RefreshToken, refresh; g != w {
t.Errorf("RefreshToken = %q, want %q", g, w)
}
if g, w := tok.Extra["id_token"], id; g != w {
t.Errorf("Extra['id_token'] = %q, want %q", g, w)
}
if tok.Expiry.IsZero() {
t.Errorf("Expiry is zero; want ~1 hour")
} else {
exp := tok.Expiry.Sub(time.Now())
const slop = 3 * time.Second // time moving during test
if (time.Hour-slop) > exp || exp > time.Hour {
t.Errorf("Expiry = %v, want ~1 hour", exp)
}
}
}
func checkBody(t *testing.T, r *http.Response, body string) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("reading reponse body: %v, want %q", err, body)
}
if g, w := string(b), body; g != w {
t.Errorf("request body mismatch: got %q, want %q", g, w)
}
}
func TestCachePermissions(t *testing.T) {
if runtime.GOOS == "windows" {
// Windows doesn't support file mode bits.
return
}
td, err := ioutil.TempDir("", "oauth-test")
if err != nil {
t.Fatalf("ioutil.TempDir: %v", err)
}
defer os.RemoveAll(td)
tempFile := filepath.Join(td, "cache-file")
cf := CacheFile(tempFile)
if err := cf.PutToken(new(Token)); err != nil {
t.Fatalf("PutToken: %v", err)
}
fi, err := os.Stat(tempFile)
if err != nil {
t.Fatalf("os.Stat: %v", err)
}
if fi.Mode()&0077 != 0 {
t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode())
}
}
func TestTokenExpired(t *testing.T) {
tests := []struct {
token Token
expired bool
}{
{Token{AccessToken: "foo"}, false},
{Token{AccessToken: ""}, true},
{Token{AccessToken: "foo", Expiry: time.Now().Add(-1 * time.Hour)}, true},
{Token{AccessToken: "foo", Expiry: time.Now().Add(1 * time.Hour)}, false},
}
for _, tt := range tests {
if got := tt.token.Expired(); got != tt.expired {
t.Errorf("token %+v Expired = %v; want %v", tt.token, got, !got)
}
}
}

View File

@@ -0,0 +1,91 @@
#!/usr/bin/perl
# 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.
#
# Test script run as a child process under cgi_test.go
use strict;
use Cwd;
binmode STDOUT;
my $q = MiniCGI->new;
my $params = $q->Vars;
if ($params->{"loc"}) {
print "Location: $params->{loc}\r\n\r\n";
exit(0);
}
print "Content-Type: text/html\r\n";
print "X-CGI-Pid: $$\r\n";
print "X-Test-Header: X-Test-Value\r\n";
print "\r\n";
if ($params->{"bigresponse"}) {
# 17 MB, for OS X: golang.org/issue/4958
for (1..(17 * 1024)) {
print "A" x 1024, "\r\n";
}
exit 0;
}
print "test=Hello CGI\r\n";
foreach my $k (sort keys %$params) {
print "param-$k=$params->{$k}\r\n";
}
foreach my $k (sort keys %ENV) {
my $clean_env = $ENV{$k};
$clean_env =~ s/[\n\r]//g;
print "env-$k=$clean_env\r\n";
}
# NOTE: msys perl returns /c/go/src/... not C:\go\....
my $dir = getcwd();
if ($^O eq 'MSWin32' || $^O eq 'msys') {
if ($dir =~ /^.:/) {
$dir =~ s!/!\\!g;
} else {
my $cmd = $ENV{'COMSPEC'} || 'c:\\windows\\system32\\cmd.exe';
$cmd =~ s!\\!/!g;
$dir = `$cmd /c cd`;
chomp $dir;
}
}
print "cwd=$dir\r\n";
# A minimal version of CGI.pm, for people without the perl-modules
# package installed. (CGI.pm used to be part of the Perl core, but
# some distros now bundle perl-base and perl-modules separately...)
package MiniCGI;
sub new {
my $class = shift;
return bless {}, $class;
}
sub Vars {
my $self = shift;
my $pairs;
if ($ENV{CONTENT_LENGTH}) {
$pairs = do { local $/; <STDIN> };
} else {
$pairs = $ENV{QUERY_STRING};
}
my $vars = {};
foreach my $kv (split(/&/, $pairs)) {
my ($k, $v) = split(/=/, $kv, 2);
$vars->{_urldecode($k)} = _urldecode($v);
}
return $vars;
}
sub _urldecode {
my $v = shift;
$v =~ tr/+/ /;
$v =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg;
return $v;
}

View File

@@ -0,0 +1 @@
0123456789

View File

@@ -0,0 +1 @@
index.html says hello

View File

@@ -0,0 +1 @@
body {}

Some files were not shown because too many files have changed in this diff Show More