mirror of https://github.com/docker/docs.git
commit
477fffb1ba
|
@ -110,6 +110,10 @@
|
|||
"ImportPath": "github.com/google/go-querystring/query",
|
||||
"Rev": "30f7a39f4a218feb5325f3aebc60c32a572a8274"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/smartystreets/go-aws-auth",
|
||||
"Rev": "1f0db8c0ee6362470abe06a94e3385927ed72a4b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/tent/http-link-go",
|
||||
"Rev": "ac974c61c2f990f4115b119354b5e0b47550e888"
|
||||
|
|
20
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/LICENSE
generated
vendored
Normal file
20
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/LICENSE
generated
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 SmartyStreets
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
86
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/README.md
generated
vendored
Normal file
86
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/README.md
generated
vendored
Normal file
|
@ -0,0 +1,86 @@
|
|||
go-aws-auth
|
||||
===========
|
||||
|
||||
[](http://godoc.org/github.com/smartystreets/go-aws-auth)
|
||||
|
||||
Go-AWS-Auth is a comprehensive, lightweight library for signing requests to Amazon Web Services.
|
||||
|
||||
It's easy to use: simply build your HTTP request and call `awsauth.Sign(req)` before sending your request over the wire.
|
||||
|
||||
|
||||
|
||||
### Supported signing mechanisms
|
||||
|
||||
- [Signed Signature Version 2](http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html)
|
||||
- [Signed Signature Version 3](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html)
|
||||
- [Signed Signature Version 4](http://docs.aws.amazon.com/general/latest/gr/signature-version-4.html)
|
||||
- [Custom S3 Authentication Scheme](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html)
|
||||
- [Security Token Service](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html)
|
||||
- [S3 Query String Authentication](http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth)
|
||||
- [IAM Role](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials)
|
||||
|
||||
For more info about AWS authentication, see the [comprehensive docs](http://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html) at AWS.
|
||||
|
||||
|
||||
### Install
|
||||
|
||||
Go get it:
|
||||
|
||||
$ go get github.com/smartystreets/go-aws-auth
|
||||
|
||||
Then import it:
|
||||
|
||||
import "github.com/smartystreets/go-aws-auth"
|
||||
|
||||
|
||||
### Using your AWS Credentials
|
||||
|
||||
The library looks for credentials in this order:
|
||||
|
||||
1. **Hard-code:** You can manually pass in an instance of `awsauth.Credentials` to any call to a signing function as a second argument:
|
||||
|
||||
```go
|
||||
awsauth.Sign(req, awsauth.Credentials{
|
||||
AccessKeyID: "Access Key ID",
|
||||
SecretAccessKey: "Secret Access Key",
|
||||
SecurityToken: "Security Token", // STS (optional)
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
2. **Environment variables:** Set the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables with your credentials. The library will automatically detect and use them. Optionally, you may also set the `AWS_SECURITY_TOKEN` environment variable if you are using temporary credentials from [STS](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html).
|
||||
|
||||
3. **IAM Role:** If running on EC2 and the credentials are neither hard-coded nor in the environment, go-aws-auth will detect the first IAM role assigned to the current EC2 instance and use those credentials.
|
||||
|
||||
(Be especially careful hard-coding credentials into your application if the code is committed to source control.)
|
||||
|
||||
|
||||
|
||||
### Signing requests
|
||||
|
||||
Just make the request, have it signed, and perform the request as you normally would.
|
||||
|
||||
```go
|
||||
url := "https://iam.amazonaws.com/?Action=ListRoles&Version=2010-05-08"
|
||||
client := new(http.Client)
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
awsauth.Sign(req) // Automatically chooses the best signing mechanism for the service
|
||||
|
||||
resp, err := client.Do(req)
|
||||
```
|
||||
|
||||
You can use `Sign` to have the library choose the best signing algorithm depending on the service, or you can specify it manually if you know what you need:
|
||||
|
||||
- `Sign2`
|
||||
- `Sign3`
|
||||
- `Sign4`
|
||||
- `SignS3` (deprecated for Sign4)
|
||||
- `SignS3Url` (for pre-signed S3 URLs; GETs only)
|
||||
|
||||
|
||||
|
||||
### Contributing
|
||||
|
||||
Please feel free to contribute! Bug fixes are more than welcome any time, as long as tests assert correct behavior. If you'd like to change an existing implementation or see a new feature, open an issue first so we can discuss it. Thanks to all contributors!
|
231
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth.go
generated
vendored
Normal file
231
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth.go
generated
vendored
Normal file
|
@ -0,0 +1,231 @@
|
|||
// Package awsauth implements AWS request signing using Signed Signature Version 2,
|
||||
// Signed Signature Version 3, and Signed Signature Version 4. Supports S3 and STS.
|
||||
package awsauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Credentials stores the information necessary to authorize with AWS and it
|
||||
// is from this information that requests are signed.
|
||||
type Credentials struct {
|
||||
AccessKeyID string
|
||||
SecretAccessKey string
|
||||
SecurityToken string `json:"Token"`
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// Sign signs a request bound for AWS. It automatically chooses the best
|
||||
// authentication scheme based on the service the request is going to.
|
||||
func Sign(req *http.Request, cred ...Credentials) *http.Request {
|
||||
service, _ := serviceAndRegion(req.URL.Host)
|
||||
sigVersion := awsSignVersion[service]
|
||||
|
||||
switch sigVersion {
|
||||
case 2:
|
||||
return Sign2(req, cred...)
|
||||
case 3:
|
||||
return Sign3(req, cred...)
|
||||
case 4:
|
||||
return Sign4(req, cred...)
|
||||
case -1:
|
||||
return SignS3(req, cred...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sign4 signs a request with Signed Signature Version 4.
|
||||
func Sign4(req *http.Request, cred ...Credentials) *http.Request {
|
||||
signMutex.Lock()
|
||||
defer signMutex.Unlock()
|
||||
keys := chooseKeys(cred)
|
||||
|
||||
// Add the X-Amz-Security-Token header when using STS
|
||||
if keys.SecurityToken != "" {
|
||||
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
|
||||
}
|
||||
|
||||
prepareRequestV4(req)
|
||||
meta := new(metadata)
|
||||
|
||||
// Task 1
|
||||
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
|
||||
|
||||
// Task 2
|
||||
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
|
||||
|
||||
// Task 3
|
||||
signingKey := signingKeyV4(keys.SecretAccessKey, meta.date, meta.region, meta.service)
|
||||
signature := signatureV4(signingKey, stringToSign)
|
||||
|
||||
req.Header.Set("Authorization", buildAuthHeaderV4(signature, meta, keys))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// Sign3 signs a request with Signed Signature Version 3.
|
||||
// If the service you're accessing supports Version 4, use that instead.
|
||||
func Sign3(req *http.Request, cred ...Credentials) *http.Request {
|
||||
signMutex.Lock()
|
||||
defer signMutex.Unlock()
|
||||
keys := chooseKeys(cred)
|
||||
|
||||
// Add the X-Amz-Security-Token header when using STS
|
||||
if keys.SecurityToken != "" {
|
||||
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
|
||||
}
|
||||
|
||||
prepareRequestV3(req)
|
||||
|
||||
// Task 1
|
||||
stringToSign := stringToSignV3(req)
|
||||
|
||||
// Task 2
|
||||
signature := signatureV3(stringToSign, keys)
|
||||
|
||||
// Task 3
|
||||
req.Header.Set("X-Amzn-Authorization", buildAuthHeaderV3(signature, keys))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// Sign2 signs a request with Signed Signature Version 2.
|
||||
// If the service you're accessing supports Version 4, use that instead.
|
||||
func Sign2(req *http.Request, cred ...Credentials) *http.Request {
|
||||
signMutex.Lock()
|
||||
defer signMutex.Unlock()
|
||||
keys := chooseKeys(cred)
|
||||
|
||||
// Add the SecurityToken parameter when using STS
|
||||
// This must be added before the signature is calculated
|
||||
if keys.SecurityToken != "" {
|
||||
v := url.Values{}
|
||||
v.Set("SecurityToken", keys.SecurityToken)
|
||||
augmentRequestQuery(req, v)
|
||||
|
||||
}
|
||||
|
||||
prepareRequestV2(req, keys)
|
||||
|
||||
stringToSign := stringToSignV2(req)
|
||||
signature := signatureV2(stringToSign, keys)
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("Signature", signature)
|
||||
|
||||
augmentRequestQuery(req, values)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// SignS3 signs a request bound for Amazon S3 using their custom
|
||||
// HTTP authentication scheme.
|
||||
func SignS3(req *http.Request, cred ...Credentials) *http.Request {
|
||||
signMutex.Lock()
|
||||
defer signMutex.Unlock()
|
||||
keys := chooseKeys(cred)
|
||||
|
||||
// Add the X-Amz-Security-Token header when using STS
|
||||
if keys.SecurityToken != "" {
|
||||
req.Header.Set("X-Amz-Security-Token", keys.SecurityToken)
|
||||
}
|
||||
|
||||
prepareRequestS3(req)
|
||||
|
||||
stringToSign := stringToSignS3(req)
|
||||
signature := signatureS3(stringToSign, keys)
|
||||
|
||||
authHeader := "AWS " + keys.AccessKeyID + ":" + signature
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// SignS3Url signs a GET request for a resource on Amazon S3 by appending
|
||||
// query string parameters containing credentials and signature. You must
|
||||
// specify an expiration date for these signed requests. After that date,
|
||||
// a request signed with this method will be rejected by S3.
|
||||
func SignS3Url(req *http.Request, expire time.Time, cred ...Credentials) *http.Request {
|
||||
signMutex.Lock()
|
||||
defer signMutex.Unlock()
|
||||
keys := chooseKeys(cred)
|
||||
|
||||
stringToSign := stringToSignS3Url("GET", expire, req.URL.Path)
|
||||
signature := signatureS3(stringToSign, keys)
|
||||
|
||||
qs := req.URL.Query()
|
||||
qs.Set("AWSAccessKeyId", keys.AccessKeyID)
|
||||
qs.Set("Signature", signature)
|
||||
qs.Set("Expires", timeToUnixEpochString(expire))
|
||||
req.URL.RawQuery = qs.Encode()
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
// expired checks to see if the temporary credentials from an IAM role are
|
||||
// within 4 minutes of expiration (The IAM documentation says that new keys
|
||||
// will be provisioned 5 minutes before the old keys expire). Credentials
|
||||
// that do not have an Expiration cannot expire.
|
||||
func (k *Credentials) expired() bool {
|
||||
if k.Expiration.IsZero() {
|
||||
// Credentials with no expiration can't expire
|
||||
return false
|
||||
}
|
||||
expireTime := k.Expiration.Add(-4 * time.Minute)
|
||||
// if t - 4 mins is before now, true
|
||||
if expireTime.Before(time.Now()) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type metadata struct {
|
||||
algorithm string
|
||||
credentialScope string
|
||||
signedHeaders string
|
||||
date string
|
||||
region string
|
||||
service string
|
||||
}
|
||||
|
||||
const (
|
||||
envAccessKeyID = "AWS_ACCESS_KEY_ID"
|
||||
envSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
|
||||
envSecurityToken = "AWS_SECURITY_TOKEN"
|
||||
)
|
||||
|
||||
var (
|
||||
awsSignVersion = map[string]int{
|
||||
"autoscaling": 4,
|
||||
"cloudfront": 4,
|
||||
"cloudformation": 4,
|
||||
"cloudsearch": 4,
|
||||
"monitoring": 4,
|
||||
"dynamodb": 4,
|
||||
"ec2": 2,
|
||||
"elasticmapreduce": 4,
|
||||
"elastictranscoder": 4,
|
||||
"elasticache": 2,
|
||||
"glacier": 4,
|
||||
"kinesis": 4,
|
||||
"redshift": 4,
|
||||
"rds": 4,
|
||||
"sdb": 2,
|
||||
"sns": 4,
|
||||
"sqs": 4,
|
||||
"s3": 4,
|
||||
"elasticbeanstalk": 4,
|
||||
"importexport": 2,
|
||||
"iam": 4,
|
||||
"route53": 3,
|
||||
"elasticloadbalancing": 4,
|
||||
"email": 3,
|
||||
}
|
||||
|
||||
signMutex sync.Mutex
|
||||
)
|
284
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth_test.go
generated
vendored
Normal file
284
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth_test.go
generated
vendored
Normal file
|
@ -0,0 +1,284 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
Convey("Given real credentials from environment variables", t, func() {
|
||||
Convey("A request (with out-of-order query string) with to IAM should succeed (assuming Administrator Access policy)", func() {
|
||||
req := newRequest("GET", "https://iam.amazonaws.com/?Version=2010-05-08&Action=ListRoles", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign4AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to S3 should succeed", func() {
|
||||
req, _ := http.NewRequest("GET", "https://s3.amazonaws.com", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign4AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to EC2 should succeed", func() {
|
||||
req := newRequest("GET", "https://ec2.amazonaws.com/?Version=2013-10-15&Action=DescribeInstances", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign2AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to SQS should succeed", func() {
|
||||
req := newRequest("POST", "https://sqs.us-west-2.amazonaws.com", url.Values{
|
||||
"Action": []string{"ListQueues"},
|
||||
})
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign4AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to SES should succeed", func() {
|
||||
req := newRequest("GET", "https://email.us-east-1.amazonaws.com/?Action=GetSendStatistics", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign3AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to Route 53 should succeed", func() {
|
||||
req := newRequest("GET", "https://route53.amazonaws.com/2013-04-01/hostedzone?maxitems=1", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign3AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("A request to SimpleDB should succeed", func() {
|
||||
req := newRequest("GET", "https://sdb.amazonaws.com/?Action=ListDomains&Version=2009-04-15", nil)
|
||||
|
||||
if !credentialsSet() {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := sign2AndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("If S3Resource env variable is set", func() {
|
||||
s3res := os.Getenv("S3Resource")
|
||||
|
||||
Convey("A URL-signed request to that S3 resource should succeed", func() {
|
||||
req, _ := http.NewRequest("GET", s3res, nil)
|
||||
|
||||
if !credentialsSet() || s3res == "" {
|
||||
SkipSo(http.StatusOK, ShouldEqual, http.StatusOK)
|
||||
} else {
|
||||
resp := signS3UrlAndDo(req)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
msg, _ := ioutil.ReadAll(resp.Body)
|
||||
t.Error(string(msg))
|
||||
}
|
||||
So(resp.StatusCode, ShouldEqual, http.StatusOK)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
Convey("Requests to services using Version 2 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("GET", "https://ec2.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req)
|
||||
So(signedReq.URL.Query().Get("SignatureVersion"), ShouldEqual, "2")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Requests to services using Version 3 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("GET", "https://route53.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req)
|
||||
So(signedReq.Header.Get("X-Amzn-Authorization"), ShouldNotBeBlank)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Requests to services using Version 4 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}),
|
||||
newRequest("GET", "https://iam.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://s3.amazonaws.com", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req)
|
||||
So(signedReq.Header.Get("Authorization"), ShouldContainSubstring, ", Signature=")
|
||||
}
|
||||
})
|
||||
|
||||
var keys Credentials
|
||||
keys = newKeys()
|
||||
Convey("Requests to services using existing credentials Version 2 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("GET", "https://ec2.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://elasticache.amazonaws.com/", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req, keys)
|
||||
So(signedReq.URL.Query().Get("SignatureVersion"), ShouldEqual, "2")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Requests to services using existing credentials Version 3 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("GET", "https://route53.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://email.us-east-1.amazonaws.com/", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req, keys)
|
||||
So(signedReq.Header.Get("X-Amzn-Authorization"), ShouldNotBeBlank)
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Requests to services using existing credentials Version 4 should be signed accordingly", t, func() {
|
||||
reqs := []*http.Request{
|
||||
newRequest("POST", "https://sqs.amazonaws.com/", url.Values{}),
|
||||
newRequest("GET", "https://iam.amazonaws.com", url.Values{}),
|
||||
newRequest("GET", "https://s3.amazonaws.com", url.Values{}),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
signedReq := Sign(req, keys)
|
||||
So(signedReq.Header.Get("Authorization"), ShouldContainSubstring, ", Signature=")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExpiration(t *testing.T) {
|
||||
var c = &Credentials{}
|
||||
|
||||
Convey("Credentials without an expiration can't expire", t, func() {
|
||||
So(c.expired(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Credentials that expire in 5 minutes aren't expired", t, func() {
|
||||
c.Expiration = time.Now().Add(5 * time.Minute)
|
||||
So(c.expired(), ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Credentials that expire in 1 minute are expired", t, func() {
|
||||
c.Expiration = time.Now().Add(1 * time.Minute)
|
||||
So(c.expired(), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Credentials that expired 2 hours ago are expired", t, func() {
|
||||
c.Expiration = time.Now().Add(-2 * time.Hour)
|
||||
So(c.expired(), ShouldBeTrue)
|
||||
})
|
||||
}
|
||||
|
||||
func credentialsSet() bool {
|
||||
var keys Credentials
|
||||
keys = newKeys()
|
||||
if keys.AccessKeyID == "" {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func newRequest(method string, url string, v url.Values) *http.Request {
|
||||
req, _ := http.NewRequest(method, url, strings.NewReader(v.Encode()))
|
||||
return req
|
||||
}
|
||||
|
||||
func sign2AndDo(req *http.Request) *http.Response {
|
||||
Sign2(req)
|
||||
resp, _ := client.Do(req)
|
||||
return resp
|
||||
}
|
||||
|
||||
func sign3AndDo(req *http.Request) *http.Response {
|
||||
Sign3(req)
|
||||
resp, _ := client.Do(req)
|
||||
return resp
|
||||
}
|
||||
|
||||
func sign4AndDo(req *http.Request) *http.Response {
|
||||
Sign4(req)
|
||||
resp, _ := client.Do(req)
|
||||
return resp
|
||||
}
|
||||
|
||||
func signS3AndDo(req *http.Request) *http.Response {
|
||||
SignS3(req)
|
||||
resp, _ := client.Do(req)
|
||||
return resp
|
||||
}
|
||||
|
||||
func signS3UrlAndDo(req *http.Request) *http.Response {
|
||||
SignS3Url(req, time.Now().AddDate(0, 0, 1))
|
||||
resp, _ := client.Do(req)
|
||||
return resp
|
||||
}
|
||||
|
||||
var client = &http.Client{}
|
300
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common.go
generated
vendored
Normal file
300
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common.go
generated
vendored
Normal file
|
@ -0,0 +1,300 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type location struct {
|
||||
ec2 bool
|
||||
checked bool
|
||||
}
|
||||
|
||||
var loc *location
|
||||
|
||||
// serviceAndRegion parsers a hostname to find out which ones it is.
|
||||
// http://docs.aws.amazon.com/general/latest/gr/rande.html
|
||||
func serviceAndRegion(host string) (service string, region string) {
|
||||
// These are the defaults if the hostname doesn't suggest something else
|
||||
region = "us-east-1"
|
||||
service = "s3"
|
||||
|
||||
parts := strings.Split(host, ".")
|
||||
if len(parts) == 4 {
|
||||
// Either service.region.amazonaws.com or virtual-host.region.amazonaws.com
|
||||
if parts[1] == "s3" {
|
||||
service = "s3"
|
||||
} else if strings.HasPrefix(parts[1], "s3-") {
|
||||
region = parts[1][3:]
|
||||
service = "s3"
|
||||
} else {
|
||||
service = parts[0]
|
||||
region = parts[1]
|
||||
}
|
||||
} else {
|
||||
// Either service.amazonaws.com or s3-region.amazonaws.com
|
||||
if strings.HasPrefix(parts[0], "s3-") {
|
||||
region = parts[0][3:]
|
||||
} else {
|
||||
service = parts[0]
|
||||
}
|
||||
}
|
||||
|
||||
if region == "external-1" {
|
||||
region = "us-east-1"
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// newKeys produces a set of credentials based on the environment
|
||||
func newKeys() (newCredentials Credentials) {
|
||||
// First use credentials from environment variables
|
||||
newCredentials.AccessKeyID = os.Getenv(envAccessKeyID)
|
||||
newCredentials.SecretAccessKey = os.Getenv(envSecretAccessKey)
|
||||
newCredentials.SecurityToken = os.Getenv(envSecurityToken)
|
||||
|
||||
// If there is no Access Key and you are on EC2, get the key from the role
|
||||
if newCredentials.AccessKeyID == "" && onEC2() {
|
||||
newCredentials = *getIAMRoleCredentials()
|
||||
}
|
||||
|
||||
// If the key is expiring, get a new key
|
||||
if newCredentials.expired() && onEC2() {
|
||||
newCredentials = *getIAMRoleCredentials()
|
||||
}
|
||||
|
||||
return newCredentials
|
||||
}
|
||||
|
||||
// checkKeys gets credentials depending on if any were passed in as an argument
|
||||
// or it makes new ones based on the environment.
|
||||
func chooseKeys(cred []Credentials) Credentials {
|
||||
if len(cred) == 0 {
|
||||
return newKeys()
|
||||
} else {
|
||||
return cred[0]
|
||||
}
|
||||
}
|
||||
|
||||
// onEC2 checks to see if the program is running on an EC2 instance.
|
||||
// It does this by looking for the EC2 metadata service.
|
||||
// This caches that information in a struct so that it doesn't waste time.
|
||||
func onEC2() bool {
|
||||
if loc == nil {
|
||||
loc = &location{}
|
||||
}
|
||||
if !(loc.checked) {
|
||||
c, err := net.DialTimeout("tcp", "169.254.169.254:80", time.Second)
|
||||
|
||||
if err != nil {
|
||||
loc.ec2 = false
|
||||
} else {
|
||||
c.Close()
|
||||
loc.ec2 = true
|
||||
}
|
||||
loc.checked = true
|
||||
}
|
||||
|
||||
return loc.ec2
|
||||
}
|
||||
|
||||
// getIAMRoleList gets a list of the roles that are available to this instance
|
||||
func getIAMRoleList() []string {
|
||||
|
||||
var roles []string
|
||||
url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
|
||||
if err != nil {
|
||||
return roles
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return roles
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
for scanner.Scan() {
|
||||
roles = append(roles, scanner.Text())
|
||||
}
|
||||
return roles
|
||||
}
|
||||
|
||||
func getIAMRoleCredentials() *Credentials {
|
||||
|
||||
roles := getIAMRoleList()
|
||||
|
||||
if len(roles) < 1 {
|
||||
return &Credentials{}
|
||||
}
|
||||
|
||||
// Use the first role in the list
|
||||
role := roles[0]
|
||||
|
||||
url := "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
|
||||
|
||||
// Create the full URL of the role
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(url)
|
||||
buffer.WriteString(role)
|
||||
roleurl := buffer.String()
|
||||
|
||||
// Get the role
|
||||
rolereq, err := http.NewRequest("GET", roleurl, nil)
|
||||
|
||||
if err != nil {
|
||||
return &Credentials{}
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
roleresp, err := client.Do(rolereq)
|
||||
|
||||
if err != nil {
|
||||
return &Credentials{}
|
||||
}
|
||||
defer roleresp.Body.Close()
|
||||
|
||||
rolebuf := new(bytes.Buffer)
|
||||
rolebuf.ReadFrom(roleresp.Body)
|
||||
|
||||
creds := Credentials{}
|
||||
|
||||
err = json.Unmarshal(rolebuf.Bytes(), &creds)
|
||||
|
||||
if err != nil {
|
||||
return &Credentials{}
|
||||
}
|
||||
|
||||
return &creds
|
||||
|
||||
}
|
||||
|
||||
func augmentRequestQuery(req *http.Request, values url.Values) *http.Request {
|
||||
for key, arr := range req.URL.Query() {
|
||||
for _, val := range arr {
|
||||
values.Set(key, val)
|
||||
}
|
||||
}
|
||||
|
||||
req.URL.RawQuery = values.Encode()
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func hmacSHA256(key []byte, content string) []byte {
|
||||
mac := hmac.New(sha256.New, key)
|
||||
mac.Write([]byte(content))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func hmacSHA1(key []byte, content string) []byte {
|
||||
mac := hmac.New(sha1.New, key)
|
||||
mac.Write([]byte(content))
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
func hashSHA256(content []byte) string {
|
||||
h := sha256.New()
|
||||
h.Write(content)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func hashMD5(content []byte) string {
|
||||
h := md5.New()
|
||||
h.Write(content)
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
func readAndReplaceBody(req *http.Request) []byte {
|
||||
if req.Body == nil {
|
||||
return []byte{}
|
||||
}
|
||||
payload, _ := ioutil.ReadAll(req.Body)
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader(payload))
|
||||
return payload
|
||||
}
|
||||
|
||||
func concat(delim string, str ...string) string {
|
||||
return strings.Join(str, delim)
|
||||
}
|
||||
|
||||
var now = func() time.Time {
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
func normuri(uri string) string {
|
||||
parts := strings.Split(uri, "/")
|
||||
for i := range parts {
|
||||
parts[i] = encodePathFrag(parts[i])
|
||||
}
|
||||
return strings.Join(parts, "/")
|
||||
}
|
||||
|
||||
func encodePathFrag(s string) string {
|
||||
hexCount := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if shouldEscape(c) {
|
||||
hexCount++
|
||||
}
|
||||
}
|
||||
t := make([]byte, len(s)+2*hexCount)
|
||||
j := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if shouldEscape(c) {
|
||||
t[j] = '%'
|
||||
t[j+1] = "0123456789ABCDEF"[c>>4]
|
||||
t[j+2] = "0123456789ABCDEF"[c&15]
|
||||
j += 3
|
||||
} else {
|
||||
t[j] = c
|
||||
j++
|
||||
}
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
|
||||
func shouldEscape(c byte) bool {
|
||||
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' {
|
||||
return false
|
||||
}
|
||||
if '0' <= c && c <= '9' {
|
||||
return false
|
||||
}
|
||||
if c == '-' || c == '_' || c == '.' || c == '~' {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func normquery(v url.Values) string {
|
||||
qs := v.Encode()
|
||||
|
||||
// Go encodes a space as '+' but Amazon require '%20'. Luckily any '+' in the
|
||||
// original query string has been percent escaped so all '+' chars that are left
|
||||
// were originally spaces.
|
||||
|
||||
return strings.Replace(qs, "+", "%20", -1)
|
||||
}
|
89
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common_test.go
generated
vendored
Normal file
89
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common_test.go
generated
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"net/url"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestCommonFunctions(t *testing.T) {
|
||||
Convey("Service and region should be properly extracted from host strings", t, func() {
|
||||
service, region := serviceAndRegion("sqs.us-west-2.amazonaws.com")
|
||||
So(service, ShouldEqual, "sqs")
|
||||
So(region, ShouldEqual, "us-west-2")
|
||||
|
||||
service, region = serviceAndRegion("iam.amazonaws.com")
|
||||
So(service, ShouldEqual, "iam")
|
||||
So(region, ShouldEqual, "us-east-1")
|
||||
|
||||
service, region = serviceAndRegion("sns.us-west-2.amazonaws.com")
|
||||
So(service, ShouldEqual, "sns")
|
||||
So(region, ShouldEqual, "us-west-2")
|
||||
|
||||
service, region = serviceAndRegion("bucketname.s3.amazonaws.com")
|
||||
So(service, ShouldEqual, "s3")
|
||||
So(region, ShouldEqual, "us-east-1")
|
||||
|
||||
service, region = serviceAndRegion("s3.amazonaws.com")
|
||||
So(service, ShouldEqual, "s3")
|
||||
So(region, ShouldEqual, "us-east-1")
|
||||
|
||||
service, region = serviceAndRegion("s3-us-west-1.amazonaws.com")
|
||||
So(service, ShouldEqual, "s3")
|
||||
So(region, ShouldEqual, "us-west-1")
|
||||
|
||||
service, region = serviceAndRegion("s3-external-1.amazonaws.com")
|
||||
So(service, ShouldEqual, "s3")
|
||||
So(region, ShouldEqual, "us-east-1")
|
||||
})
|
||||
|
||||
Convey("MD5 hashes should be properly computed and base-64 encoded", t, func() {
|
||||
input := []byte("Pretend this is a REALLY long byte array...")
|
||||
actual := hashMD5(input)
|
||||
|
||||
So(actual, ShouldEqual, "KbVTY8Vl6VccnzQf1AGOFw==")
|
||||
})
|
||||
|
||||
Convey("SHA-256 hashes should be properly hex-encoded (base 16)", t, func() {
|
||||
input := []byte("This is... Sparta!!")
|
||||
actual := hashSHA256(input)
|
||||
|
||||
So(actual, ShouldEqual, "5c81a4ef1172e89b1a9d575f4cd82f4ed20ea9137e61aa7f1ab936291d24e79a")
|
||||
})
|
||||
|
||||
Convey("Given a key and contents", t, func() {
|
||||
key := []byte("asdf1234")
|
||||
contents := "SmartyStreets was here"
|
||||
|
||||
Convey("HMAC-SHA256 should be properly computed", func() {
|
||||
expected := []byte{65, 46, 186, 78, 2, 155, 71, 104, 49, 37, 5, 66, 195, 129, 159, 227, 239, 53, 240, 107, 83, 21, 235, 198, 238, 216, 108, 149, 143, 222, 144, 94}
|
||||
actual := hmacSHA256(key, contents)
|
||||
|
||||
So(actual, ShouldResemble, expected)
|
||||
})
|
||||
|
||||
Convey("HMAC-SHA1 should be properly computed", func() {
|
||||
expected := []byte{164, 77, 252, 0, 87, 109, 207, 110, 163, 75, 228, 122, 83, 255, 233, 237, 125, 206, 85, 70}
|
||||
actual := hmacSHA1(key, contents)
|
||||
|
||||
So(actual, ShouldResemble, expected)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Strings should be properly concatenated with a delimiter", t, func() {
|
||||
So(concat("\n", "Test1", "Test2"), ShouldEqual, "Test1\nTest2")
|
||||
So(concat(".", "Test1"), ShouldEqual, "Test1")
|
||||
So(concat("\t", "1", "2", "3", "4"), ShouldEqual, "1\t2\t3\t4")
|
||||
})
|
||||
|
||||
Convey("URI components should be properly encoded", t, func() {
|
||||
So(normuri("/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"), ShouldEqual, "/-._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||
So(normuri("/ /foo"), ShouldEqual, "/%20/foo")
|
||||
So(normuri("/(foo)"), ShouldEqual, "/%28foo%29")
|
||||
})
|
||||
|
||||
Convey("URI query strings should be properly encoded", t, func() {
|
||||
So(normquery(url.Values{"p": []string{" +&;-=._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"}}), ShouldEqual, "p=%20%2B%26%3B-%3D._~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
|
||||
})
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func signatureS3(stringToSign string, keys Credentials) string {
|
||||
hashed := hmacSHA1([]byte(keys.SecretAccessKey), stringToSign)
|
||||
return base64.StdEncoding.EncodeToString(hashed)
|
||||
}
|
||||
|
||||
func stringToSignS3(req *http.Request) string {
|
||||
str := req.Method + "\n"
|
||||
|
||||
if req.Header.Get("Content-Md5") != "" {
|
||||
str += req.Header.Get("Content-Md5")
|
||||
} else {
|
||||
body := readAndReplaceBody(req)
|
||||
if len(body) > 0 {
|
||||
str += hashMD5(body)
|
||||
}
|
||||
}
|
||||
str += "\n"
|
||||
|
||||
str += req.Header.Get("Content-Type") + "\n"
|
||||
|
||||
if req.Header.Get("Date") != "" {
|
||||
str += req.Header.Get("Date")
|
||||
} else {
|
||||
str += timestampS3()
|
||||
}
|
||||
|
||||
str += "\n"
|
||||
|
||||
canonicalHeaders := canonicalAmzHeadersS3(req)
|
||||
if canonicalHeaders != "" {
|
||||
str += canonicalHeaders
|
||||
}
|
||||
|
||||
str += canonicalResourceS3(req)
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
func stringToSignS3Url(method string, expire time.Time, path string) string {
|
||||
return method + "\n\n\n" + timeToUnixEpochString(expire) + "\n" + path
|
||||
}
|
||||
|
||||
func timeToUnixEpochString(t time.Time) string {
|
||||
return strconv.FormatInt(t.Unix(), 10)
|
||||
}
|
||||
|
||||
func canonicalAmzHeadersS3(req *http.Request) string {
|
||||
var headers []string
|
||||
|
||||
for header := range req.Header {
|
||||
standardized := strings.ToLower(strings.TrimSpace(header))
|
||||
if strings.HasPrefix(standardized, "x-amz") {
|
||||
headers = append(headers, standardized)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(headers)
|
||||
|
||||
for i, header := range headers {
|
||||
headers[i] = header + ":" + strings.Replace(req.Header.Get(header), "\n", " ", -1)
|
||||
}
|
||||
|
||||
if len(headers) > 0 {
|
||||
return strings.Join(headers, "\n") + "\n"
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalResourceS3(req *http.Request) string {
|
||||
res := ""
|
||||
|
||||
if isS3VirtualHostedStyle(req) {
|
||||
bucketname := strings.Split(req.Host, ".")[0]
|
||||
res += "/" + bucketname
|
||||
}
|
||||
|
||||
res += req.URL.Path
|
||||
|
||||
for _, subres := range strings.Split(subresourcesS3, ",") {
|
||||
if strings.HasPrefix(req.URL.RawQuery, subres) {
|
||||
res += "?" + subres
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func prepareRequestS3(req *http.Request) *http.Request {
|
||||
req.Header.Set("Date", timestampS3())
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path += "/"
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
// Info: http://docs.aws.amazon.com/AmazonS3/latest/dev/VirtualHosting.html
|
||||
func isS3VirtualHostedStyle(req *http.Request) bool {
|
||||
service, _ := serviceAndRegion(req.Host)
|
||||
return service == "s3" && strings.Count(req.Host, ".") == 3
|
||||
}
|
||||
|
||||
func timestampS3() string {
|
||||
return now().Format(timeFormatS3)
|
||||
}
|
||||
|
||||
const (
|
||||
timeFormatS3 = time.RFC1123Z
|
||||
subresourcesS3 = "acl,lifecycle,location,logging,notification,partNumber,policy,requestPayment,torrent,uploadId,uploads,versionId,versioning,versions,website"
|
||||
)
|
152
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3_test.go
generated
vendored
Normal file
152
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3_test.go
generated
vendored
Normal file
|
@ -0,0 +1,152 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSignatureS3(t *testing.T) {
|
||||
// http://docs.aws.amazon.com/AmazonS3/2006-03-01/dev/RESTAuthentication.html
|
||||
// Note: S3 now supports signed signature version 4
|
||||
// (but signed URL requests still utilize a lot of the same functionality)
|
||||
|
||||
Convey("Given a GET request to Amazon S3", t, func() {
|
||||
keys := *testCredS3
|
||||
req := test_plainRequestS3()
|
||||
|
||||
// Mock time
|
||||
now = func() time.Time {
|
||||
parsed, _ := time.Parse(timeFormatS3, exampleReqTsS3)
|
||||
return parsed
|
||||
}
|
||||
|
||||
Convey("The request should be prepared with a Date header", func() {
|
||||
prepareRequestS3(req)
|
||||
So(req.Header.Get("Date"), ShouldEqual, exampleReqTsS3)
|
||||
})
|
||||
|
||||
Convey("The CanonicalizedAmzHeaders should be built properly", func() {
|
||||
req2 := test_headerRequestS3()
|
||||
actual := canonicalAmzHeadersS3(req2)
|
||||
So(actual, ShouldEqual, expectedCanonAmzHeadersS3)
|
||||
})
|
||||
|
||||
Convey("The CanonicalizedResource should be built properly", func() {
|
||||
actual := canonicalResourceS3(req)
|
||||
So(actual, ShouldEqual, expectedCanonResourceS3)
|
||||
})
|
||||
|
||||
Convey("The string to sign should be correct", func() {
|
||||
actual := stringToSignS3(req)
|
||||
So(actual, ShouldEqual, expectedStringToSignS3)
|
||||
})
|
||||
|
||||
Convey("The final signature string should be exactly correct", func() {
|
||||
actual := signatureS3(stringToSignS3(req), keys)
|
||||
So(actual, ShouldEqual, "bWq2s1WEIj+Ydj0vQ697zp+IXMU=")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a GET request for a resource on S3 for query string authentication", t, func() {
|
||||
keys := *testCredS3
|
||||
req, _ := http.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg", nil)
|
||||
|
||||
now = func() time.Time {
|
||||
parsed, _ := time.Parse(timeFormatS3, exampleReqTsS3)
|
||||
return parsed
|
||||
}
|
||||
|
||||
Convey("The string to sign should be correct", func() {
|
||||
actual := stringToSignS3Url("GET", now(), req.URL.Path)
|
||||
So(actual, ShouldEqual, expectedStringToSignS3Url)
|
||||
})
|
||||
|
||||
Convey("The signature of string to sign should be correct", func() {
|
||||
actual := signatureS3(expectedStringToSignS3Url, keys)
|
||||
So(actual, ShouldEqual, "R2K/+9bbnBIbVDCs7dqlz3XFtBQ=")
|
||||
})
|
||||
|
||||
Convey("The finished signed URL should be correct", func() {
|
||||
expiry := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||
So(SignS3Url(req, expiry, keys).URL.String(), ShouldEqual, expectedSignedS3Url)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestS3STSRequestPreparer(t *testing.T) {
|
||||
Convey("Given a plain request with no custom headers", t, func() {
|
||||
req := test_plainRequestS3()
|
||||
|
||||
Convey("And a set of credentials with an STS token", func() {
|
||||
keys := *testCredS3WithSTS
|
||||
|
||||
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
|
||||
actualSigned := SignS3(req, keys)
|
||||
actual := actualSigned.Header.Get("X-Amz-Security-Token")
|
||||
|
||||
So(actual, ShouldNotBeBlank)
|
||||
So(actual, ShouldEqual, testCredS3WithSTS.SecurityToken)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func test_plainRequestS3() *http.Request {
|
||||
req, _ := http.NewRequest("GET", "https://johnsmith.s3.amazonaws.com/photos/puppy.jpg", nil)
|
||||
return req
|
||||
}
|
||||
|
||||
func test_headerRequestS3() *http.Request {
|
||||
req := test_plainRequestS3()
|
||||
req.Header.Set("X-Amz-Meta-Something", "more foobar")
|
||||
req.Header.Set("X-Amz-Date", "foobar")
|
||||
req.Header.Set("X-Foobar", "nanoo-nanoo")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestCanonical(t *testing.T) {
|
||||
expectedCanonicalString := "PUT\nc8fdb181845a4ca6b8fec737b3581d76\ntext/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\nx-amz-meta-author:foo@bar.com\n/quotes/nelson"
|
||||
|
||||
origUrl := "https://s3.amazonaws.com/"
|
||||
resource := "/quotes/nelson"
|
||||
|
||||
u, _ := url.ParseRequestURI(origUrl)
|
||||
u.Path = resource
|
||||
urlStr := fmt.Sprintf("%v", u)
|
||||
|
||||
req, _ := http.NewRequest("PUT", urlStr, nil)
|
||||
req.Header.Add("Content-Md5", "c8fdb181845a4ca6b8fec737b3581d76")
|
||||
req.Header.Add("Content-Type", "text/html")
|
||||
req.Header.Add("Date", "Thu, 17 Nov 2005 18:49:58 GMT")
|
||||
req.Header.Add("X-Amz-Meta-Author", "foo@bar.com")
|
||||
req.Header.Add("X-Amz-Magic", "abracadabra")
|
||||
|
||||
if stringToSignS3(req) != expectedCanonicalString {
|
||||
t.Errorf("----Got\n***%s***\n----Expected\n***%s***", stringToSignS3(req), expectedCanonicalString)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
testCredS3 = &Credentials{
|
||||
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
}
|
||||
|
||||
testCredS3WithSTS = &Credentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
|
||||
}
|
||||
|
||||
expectedCanonAmzHeadersS3 = "x-amz-date:foobar\nx-amz-meta-something:more foobar\n"
|
||||
expectedCanonResourceS3 = "/johnsmith/photos/puppy.jpg"
|
||||
expectedStringToSignS3 = "GET\n\n\nTue, 27 Mar 2007 19:36:42 +0000\n/johnsmith/photos/puppy.jpg"
|
||||
expectedStringToSignS3Url = "GET\n\n\n1175024202\n/johnsmith/photos/puppy.jpg"
|
||||
expectedSignedS3Url = "https://johnsmith.s3.amazonaws.com/johnsmith/photos/puppy.jpg?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Expires=1257894000&Signature=X%2FarTLAJP08uP1Bsap52rwmsVok%3D"
|
||||
exampleReqTsS3 = "Tue, 27 Mar 2007 19:36:42 +0000"
|
||||
)
|
50
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2.go
generated
vendored
Normal file
50
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2.go
generated
vendored
Normal file
|
@ -0,0 +1,50 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func prepareRequestV2(req *http.Request, keys Credentials) *http.Request {
|
||||
|
||||
keyID := keys.AccessKeyID
|
||||
|
||||
values := url.Values{}
|
||||
values.Set("AWSAccessKeyId", keyID)
|
||||
values.Set("SignatureVersion", "2")
|
||||
values.Set("SignatureMethod", "HmacSHA256")
|
||||
values.Set("Timestamp", timestampV2())
|
||||
|
||||
augmentRequestQuery(req, values)
|
||||
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path += "/"
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func stringToSignV2(req *http.Request) string {
|
||||
str := req.Method + "\n"
|
||||
str += strings.ToLower(req.URL.Host) + "\n"
|
||||
str += req.URL.Path + "\n"
|
||||
str += canonicalQueryStringV2(req)
|
||||
return str
|
||||
}
|
||||
|
||||
func signatureV2(strToSign string, keys Credentials) string {
|
||||
hashed := hmacSHA256([]byte(keys.SecretAccessKey), strToSign)
|
||||
return base64.StdEncoding.EncodeToString(hashed)
|
||||
}
|
||||
|
||||
func canonicalQueryStringV2(req *http.Request) string {
|
||||
return req.URL.RawQuery
|
||||
}
|
||||
|
||||
func timestampV2() string {
|
||||
return now().Format(timeFormatV2)
|
||||
}
|
||||
|
||||
const timeFormatV2 = "2006-01-02T15:04:05"
|
125
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2_test.go
generated
vendored
Normal file
125
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2_test.go
generated
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSignature2(t *testing.T) {
|
||||
// http://docs.aws.amazon.com/general/latest/gr/signature-version-2.html
|
||||
|
||||
Convey("Given bogus credentials", t, func() {
|
||||
keys := *testCredV2
|
||||
|
||||
// Mock time
|
||||
now = func() time.Time {
|
||||
parsed, _ := time.Parse(timeFormatV2, exampleReqTsV2)
|
||||
return parsed
|
||||
}
|
||||
|
||||
Convey("Given a plain request that is unprepared", func() {
|
||||
req := test_plainRequestV2()
|
||||
|
||||
Convey("The request should be prepared to be signed", func() {
|
||||
expectedUnsigned := test_unsignedRequestV2()
|
||||
prepareRequestV2(req, keys)
|
||||
So(req, ShouldResemble, expectedUnsigned)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a prepared, but unsigned, request", func() {
|
||||
req := test_unsignedRequestV2()
|
||||
|
||||
Convey("The canonical query string should be correct", func() {
|
||||
actual := canonicalQueryStringV2(req)
|
||||
expected := canonicalQsV2
|
||||
So(actual, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("The absolute path should be extracted correctly", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/")
|
||||
})
|
||||
|
||||
Convey("The string to sign should be well-formed", func() {
|
||||
actual := stringToSignV2(req)
|
||||
So(actual, ShouldEqual, expectedStringToSignV2)
|
||||
})
|
||||
|
||||
Convey("The resulting signature should be correct", func() {
|
||||
actual := signatureV2(stringToSignV2(req), keys)
|
||||
So(actual, ShouldEqual, "i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf/Mj6vPxyYIs=")
|
||||
})
|
||||
|
||||
Convey("The final signed request should be correctly formed", func() {
|
||||
Sign2(req, keys)
|
||||
actual := req.URL.String()
|
||||
So(actual, ShouldResemble, expectedFinalUrlV2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersion2STSRequestPreparer(t *testing.T) {
|
||||
Convey("Given a plain request ", t, func() {
|
||||
req := test_plainRequestV2()
|
||||
|
||||
Convey("And a set of credentials with an STS token", func() {
|
||||
var keys Credentials
|
||||
keys = *testCredV2WithSTS
|
||||
|
||||
Convey("It should include the SecurityToken parameter when the request is signed", func() {
|
||||
actualSigned := Sign2(req, keys)
|
||||
actual := actualSigned.URL.Query()["SecurityToken"][0]
|
||||
|
||||
So(actual, ShouldNotBeBlank)
|
||||
So(actual, ShouldEqual, testCredV2WithSTS.SecurityToken)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func test_plainRequestV2() *http.Request {
|
||||
values := url.Values{}
|
||||
values.Set("Action", "DescribeJobFlows")
|
||||
values.Set("Version", "2009-03-31")
|
||||
|
||||
url := baseUrlV2 + "?" + values.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func test_unsignedRequestV2() *http.Request {
|
||||
req := test_plainRequestV2()
|
||||
newUrl, _ := url.Parse(baseUrlV2 + "/?" + canonicalQsV2)
|
||||
req.URL = newUrl
|
||||
return req
|
||||
}
|
||||
|
||||
var (
|
||||
testCredV2 = &Credentials{
|
||||
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
}
|
||||
|
||||
testCredV2WithSTS = &Credentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
|
||||
}
|
||||
|
||||
exampleReqTsV2 = "2011-10-03T15:19:30"
|
||||
baseUrlV2 = "https://elasticmapreduce.amazonaws.com"
|
||||
canonicalQsV2 = "AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31"
|
||||
expectedStringToSignV2 = "GET\nelasticmapreduce.amazonaws.com\n/\n" + canonicalQsV2
|
||||
expectedFinalUrlV2 = baseUrlV2 + "/?AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE&Action=DescribeJobFlows&Signature=i91nKc4PWAt0JJIdXwz9HxZCJDdiy6cf%2FMj6vPxyYIs%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2011-10-03T15%3A19%3A30&Version=2009-03-31"
|
||||
)
|
58
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3.go
generated
vendored
Normal file
58
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3.go
generated
vendored
Normal file
|
@ -0,0 +1,58 @@
|
|||
// Thanks to Michael Vierling for contributing sign3.go
|
||||
|
||||
package awsauth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func stringToSignV3(req *http.Request) string {
|
||||
// TASK 1. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#StringToSign
|
||||
|
||||
return req.Header.Get("Date") + req.Header.Get("x-amz-nonce")
|
||||
}
|
||||
|
||||
func signatureV3(stringToSign string, keys Credentials) string {
|
||||
// TASK 2. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#Signature
|
||||
|
||||
hash := hmacSHA256([]byte(keys.SecretAccessKey), stringToSign)
|
||||
return base64.StdEncoding.EncodeToString(hash)
|
||||
}
|
||||
|
||||
func buildAuthHeaderV3(signature string, keys Credentials) string {
|
||||
// TASK 3. http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html#AuthorizationHeader
|
||||
|
||||
return "AWS3-HTTPS AWSAccessKeyId=" + keys.AccessKeyID +
|
||||
", Algorithm=HmacSHA256" +
|
||||
", Signature=" + signature
|
||||
}
|
||||
|
||||
func prepareRequestV3(req *http.Request) *http.Request {
|
||||
ts := timestampV3()
|
||||
necessaryDefaults := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"x-amz-date": ts,
|
||||
"Date": ts,
|
||||
"x-amz-nonce": "",
|
||||
}
|
||||
|
||||
for header, value := range necessaryDefaults {
|
||||
if req.Header.Get(header) == "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path += "/"
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func timestampV3() string {
|
||||
return now().Format(timeFormatV3)
|
||||
}
|
||||
|
||||
const timeFormatV3 = time.RFC1123
|
121
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3_test.go
generated
vendored
Normal file
121
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3_test.go
generated
vendored
Normal file
|
@ -0,0 +1,121 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestSignature3(t *testing.T) {
|
||||
// http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/RESTAuthentication.html
|
||||
// http://docs.aws.amazon.com/ses/latest/DeveloperGuide/query-interface-authentication.html
|
||||
|
||||
Convey("Given bogus credentials", t, func() {
|
||||
keys := *testCredV3
|
||||
|
||||
// Mock time
|
||||
now = func() time.Time {
|
||||
parsed, _ := time.Parse(timeFormatV3, exampleReqTsV3)
|
||||
return parsed
|
||||
}
|
||||
|
||||
Convey("Given a plain request that is unprepared", func() {
|
||||
req := test_plainRequestV3()
|
||||
|
||||
Convey("The request should be prepared to be signed", func() {
|
||||
expectedUnsigned := test_unsignedRequestV3()
|
||||
prepareRequestV3(req)
|
||||
So(req, ShouldResemble, expectedUnsigned)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a prepared, but unsigned, request", func() {
|
||||
req := test_unsignedRequestV3()
|
||||
|
||||
Convey("The absolute path should be extracted correctly", func() {
|
||||
So(req.URL.Path, ShouldEqual, "/")
|
||||
})
|
||||
|
||||
Convey("The string to sign should be well-formed", func() {
|
||||
actual := stringToSignV3(req)
|
||||
So(actual, ShouldEqual, expectedStringToSignV3)
|
||||
})
|
||||
|
||||
Convey("The resulting signature should be correct", func() {
|
||||
actual := signatureV3(stringToSignV3(req), keys)
|
||||
So(actual, ShouldEqual, "PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk=")
|
||||
})
|
||||
|
||||
Convey("The final signed request should be correctly formed", func() {
|
||||
Sign3(req, keys)
|
||||
actual := req.Header.Get("X-Amzn-Authorization")
|
||||
So(actual, ShouldResemble, expectedAuthHeaderV3)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func test_plainRequestV3() *http.Request {
|
||||
values := url.Values{}
|
||||
values.Set("Action", "GetSendStatistics")
|
||||
values.Set("Version", "2010-12-01")
|
||||
|
||||
url := baseUrlV3 + "/?" + values.Encode()
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func test_unsignedRequestV3() *http.Request {
|
||||
req := test_plainRequestV3()
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||
req.Header.Set("x-amz-date", exampleReqTsV3)
|
||||
req.Header.Set("Date", exampleReqTsV3)
|
||||
req.Header.Set("x-amz-nonce", "")
|
||||
return req
|
||||
}
|
||||
|
||||
func TestVersion3STSRequestPreparer(t *testing.T) {
|
||||
Convey("Given a plain request with no custom headers", t, func() {
|
||||
req := test_plainRequestV3()
|
||||
|
||||
Convey("And a set of credentials with an STS token", func() {
|
||||
var keys Credentials
|
||||
keys = *testCredV3WithSTS
|
||||
|
||||
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
|
||||
actualSigned := Sign3(req, keys)
|
||||
actual := actualSigned.Header.Get("X-Amz-Security-Token")
|
||||
|
||||
So(actual, ShouldNotBeBlank)
|
||||
So(actual, ShouldEqual, testCredV4WithSTS.SecurityToken)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
var (
|
||||
testCredV3 = &Credentials{
|
||||
AccessKeyID: "AKIAIOSFODNN7EXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
}
|
||||
|
||||
testCredV3WithSTS = &Credentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
|
||||
}
|
||||
|
||||
exampleReqTsV3 = "Thu, 14 Aug 2008 17:08:48 GMT"
|
||||
baseUrlV3 = "https://email.us-east-1.amazonaws.com"
|
||||
expectedStringToSignV3 = exampleReqTsV3
|
||||
expectedAuthHeaderV3 = "AWS3-HTTPS AWSAccessKeyId=" + testCredV3.AccessKeyID + ", Algorithm=HmacSHA256, Signature=PjAJ6buiV6l4WyzmmuwtKE59NJXVg5Dr3Sn4PCMZ0Yk="
|
||||
)
|
107
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4.go
generated
vendored
Normal file
107
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4.go
generated
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func hashedCanonicalRequestV4(req *http.Request, meta *metadata) string {
|
||||
// TASK 1. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
|
||||
payload := readAndReplaceBody(req)
|
||||
payloadHash := hashSHA256(payload)
|
||||
req.Header.Set("X-Amz-Content-Sha256", payloadHash)
|
||||
|
||||
// Set this in header values to make it appear in the range of headers to sign
|
||||
req.Header.Set("Host", req.Host)
|
||||
|
||||
var sortedHeaderKeys []string
|
||||
for key, _ := range req.Header {
|
||||
switch key {
|
||||
case "Content-Type", "Content-Md5", "Host":
|
||||
default:
|
||||
if !strings.HasPrefix(key, "X-Amz-") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
sortedHeaderKeys = append(sortedHeaderKeys, strings.ToLower(key))
|
||||
}
|
||||
sort.Strings(sortedHeaderKeys)
|
||||
|
||||
var headersToSign string
|
||||
for _, key := range sortedHeaderKeys {
|
||||
value := strings.TrimSpace(req.Header.Get(key))
|
||||
headersToSign += key + ":" + value + "\n"
|
||||
}
|
||||
meta.signedHeaders = concat(";", sortedHeaderKeys...)
|
||||
canonicalRequest := concat("\n", req.Method, normuri(req.URL.Path), normquery(req.URL.Query()), headersToSign, meta.signedHeaders, payloadHash)
|
||||
|
||||
return hashSHA256([]byte(canonicalRequest))
|
||||
}
|
||||
|
||||
func stringToSignV4(req *http.Request, hashedCanonReq string, meta *metadata) string {
|
||||
// TASK 2. http://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
|
||||
requestTs := req.Header.Get("X-Amz-Date")
|
||||
|
||||
meta.algorithm = "AWS4-HMAC-SHA256"
|
||||
meta.service, meta.region = serviceAndRegion(req.Host)
|
||||
meta.date = tsDateV4(requestTs)
|
||||
meta.credentialScope = concat("/", meta.date, meta.region, meta.service, "aws4_request")
|
||||
|
||||
return concat("\n", meta.algorithm, requestTs, meta.credentialScope, hashedCanonReq)
|
||||
}
|
||||
|
||||
func signatureV4(signingKey []byte, stringToSign string) string {
|
||||
// TASK 3. http://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
|
||||
|
||||
return hex.EncodeToString(hmacSHA256(signingKey, stringToSign))
|
||||
}
|
||||
|
||||
func prepareRequestV4(req *http.Request) *http.Request {
|
||||
necessaryDefaults := map[string]string{
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
"X-Amz-Date": timestampV4(),
|
||||
}
|
||||
|
||||
for header, value := range necessaryDefaults {
|
||||
if req.Header.Get(header) == "" {
|
||||
req.Header.Set(header, value)
|
||||
}
|
||||
}
|
||||
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path += "/"
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func signingKeyV4(secretKey, date, region, service string) []byte {
|
||||
kDate := hmacSHA256([]byte("AWS4"+secretKey), date)
|
||||
kRegion := hmacSHA256(kDate, region)
|
||||
kService := hmacSHA256(kRegion, service)
|
||||
kSigning := hmacSHA256(kService, "aws4_request")
|
||||
return kSigning
|
||||
}
|
||||
|
||||
func buildAuthHeaderV4(signature string, meta *metadata, keys Credentials) string {
|
||||
credential := keys.AccessKeyID + "/" + meta.credentialScope
|
||||
|
||||
return meta.algorithm +
|
||||
" Credential=" + credential +
|
||||
", SignedHeaders=" + meta.signedHeaders +
|
||||
", Signature=" + signature
|
||||
}
|
||||
|
||||
func timestampV4() string {
|
||||
return now().Format(timeFormatV4)
|
||||
}
|
||||
|
||||
func tsDateV4(timestamp string) string {
|
||||
return timestamp[:8]
|
||||
}
|
||||
|
||||
const timeFormatV4 = "20060102T150405Z"
|
216
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4_test.go
generated
vendored
Normal file
216
Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4_test.go
generated
vendored
Normal file
|
@ -0,0 +1,216 @@
|
|||
package awsauth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestVersion4RequestPreparer(t *testing.T) {
|
||||
Convey("Given a plain request with no custom headers", t, func() {
|
||||
req := test_plainRequestV4(false)
|
||||
|
||||
expectedUnsigned := test_unsignedRequestV4(true, false)
|
||||
expectedUnsigned.Header.Set("X-Amz-Date", timestampV4())
|
||||
|
||||
Convey("The necessary, default headers should be appended", func() {
|
||||
prepareRequestV4(req)
|
||||
So(req, ShouldResemble, expectedUnsigned)
|
||||
})
|
||||
|
||||
Convey("Forward-slash should be appended to URI if not present", func() {
|
||||
prepareRequestV4(req)
|
||||
So(req.URL.Path, ShouldEqual, "/")
|
||||
})
|
||||
|
||||
Convey("And a set of credentials", func() {
|
||||
var keys Credentials
|
||||
keys = *testCredV4
|
||||
|
||||
Convey("It should be signed with an Authorization header", func() {
|
||||
actualSigned := Sign4(req, keys)
|
||||
actual := actualSigned.Header.Get("Authorization")
|
||||
|
||||
So(actual, ShouldNotBeBlank)
|
||||
So(actual, ShouldContainSubstring, "Credential="+testCredV4.AccessKeyID)
|
||||
So(actual, ShouldContainSubstring, "SignedHeaders=")
|
||||
So(actual, ShouldContainSubstring, "Signature=")
|
||||
So(actual, ShouldContainSubstring, "AWS4")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given a request with custom, necessary headers", t, func() {
|
||||
Convey("The custom, necessary headers must not be changed", func() {
|
||||
req := test_unsignedRequestV4(true, false)
|
||||
prepareRequestV4(req)
|
||||
So(req, ShouldResemble, test_unsignedRequestV4(true, false))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestVersion4STSRequestPreparer(t *testing.T) {
|
||||
Convey("Given a plain request with no custom headers", t, func() {
|
||||
req := test_plainRequestV4(false)
|
||||
|
||||
Convey("And a set of credentials with an STS token", func() {
|
||||
var keys Credentials
|
||||
keys = *testCredV4WithSTS
|
||||
|
||||
Convey("It should include an X-Amz-Security-Token when the request is signed", func() {
|
||||
actualSigned := Sign4(req, keys)
|
||||
actual := actualSigned.Header.Get("X-Amz-Security-Token")
|
||||
|
||||
So(actual, ShouldNotBeBlank)
|
||||
So(actual, ShouldEqual, testCredV4WithSTS.SecurityToken)
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestVersion4SigningTasks(t *testing.T) {
|
||||
// http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
|
||||
|
||||
Convey("Given a bogus request and credentials from AWS documentation with an additional meta tag", t, func() {
|
||||
req := test_unsignedRequestV4(true, true)
|
||||
meta := new(metadata)
|
||||
|
||||
Convey("(Task 1) The canonical request should be built correctly", func() {
|
||||
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
|
||||
|
||||
So(hashedCanonReq, ShouldEqual, expectingV4["CanonicalHash"])
|
||||
})
|
||||
|
||||
Convey("(Task 2) The string to sign should be built correctly", func() {
|
||||
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
|
||||
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
|
||||
|
||||
So(stringToSign, ShouldEqual, expectingV4["StringToSign"])
|
||||
})
|
||||
|
||||
Convey("(Task 3) The version 4 signed signature should be correct", func() {
|
||||
hashedCanonReq := hashedCanonicalRequestV4(req, meta)
|
||||
stringToSign := stringToSignV4(req, hashedCanonReq, meta)
|
||||
signature := signatureV4(test_signingKeyV4(), stringToSign)
|
||||
|
||||
So(signature, ShouldEqual, expectingV4["SignatureV4"])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSignature4Helpers(t *testing.T) {
|
||||
|
||||
keys := *testCredV4
|
||||
|
||||
Convey("The signing key should be properly generated", t, func() {
|
||||
expected := []byte{152, 241, 216, 137, 254, 196, 244, 66, 26, 220, 82, 43, 171, 12, 225, 248, 46, 105, 41, 194, 98, 237, 21, 229, 169, 76, 144, 239, 209, 227, 176, 231}
|
||||
actual := test_signingKeyV4()
|
||||
|
||||
So(actual, ShouldResemble, expected)
|
||||
})
|
||||
|
||||
Convey("Authorization headers should be built properly", t, func() {
|
||||
meta := &metadata{
|
||||
algorithm: "AWS4-HMAC-SHA256",
|
||||
credentialScope: "20110909/us-east-1/iam/aws4_request",
|
||||
signedHeaders: "content-type;host;x-amz-date",
|
||||
}
|
||||
expected := expectingV4["AuthHeader"] + expectingV4["SignatureV4"]
|
||||
actual := buildAuthHeaderV4(expectingV4["SignatureV4"], meta, keys)
|
||||
|
||||
So(actual, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("Timestamps should be in the correct format, in UTC time", t, func() {
|
||||
actual := timestampV4()
|
||||
|
||||
So(len(actual), ShouldEqual, 16)
|
||||
So(actual, ShouldNotContainSubstring, ":")
|
||||
So(actual, ShouldNotContainSubstring, "-")
|
||||
So(actual, ShouldNotContainSubstring, " ")
|
||||
So(actual, ShouldEndWith, "Z")
|
||||
So(actual, ShouldContainSubstring, "T")
|
||||
})
|
||||
|
||||
Convey("Given an Version 4 AWS-formatted timestamp", t, func() {
|
||||
ts := "20110909T233600Z"
|
||||
|
||||
Convey("The date string should be extracted properly", func() {
|
||||
So(tsDateV4(ts), ShouldEqual, "20110909")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Given any request with a body", t, func() {
|
||||
req := test_plainRequestV4(false)
|
||||
|
||||
Convey("Its body should be read and replaced without differences", func() {
|
||||
expected := []byte(requestValuesV4.Encode())
|
||||
|
||||
actual1 := readAndReplaceBody(req)
|
||||
So(actual1, ShouldResemble, expected)
|
||||
|
||||
actual2 := readAndReplaceBody(req)
|
||||
So(actual2, ShouldResemble, expected)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func test_plainRequestV4(trailingSlash bool) *http.Request {
|
||||
url := "http://iam.amazonaws.com"
|
||||
body := strings.NewReader(requestValuesV4.Encode())
|
||||
|
||||
if trailingSlash {
|
||||
url += "/"
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func test_unsignedRequestV4(trailingSlash, tag bool) *http.Request {
|
||||
req := test_plainRequestV4(trailingSlash)
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded; charset=utf-8")
|
||||
req.Header.Set("X-Amz-Date", "20110909T233600Z")
|
||||
if tag {
|
||||
req.Header.Set("X-Amz-Meta-Foo", "Bar!")
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func test_signingKeyV4() []byte {
|
||||
return signingKeyV4(testCredV4.SecretAccessKey, "20110909", "us-east-1", "iam")
|
||||
}
|
||||
|
||||
var (
|
||||
testCredV4 = &Credentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
}
|
||||
|
||||
testCredV4WithSTS = &Credentials{
|
||||
AccessKeyID: "AKIDEXAMPLE",
|
||||
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
|
||||
SecurityToken: "AQoDYXdzEHcaoAJ1Aqwx1Sum0iW2NQjXJcWlKR7vuB6lnAeGBaQnjDRZPVyniwc48ml5hx+0qiXenVJdfusMMl9XLhSncfhx9Rb1UF8IAOaQ+CkpWXvoH67YYN+93dgckSVgVEBRByTl/BvLOZhe0ii/pOWkuQtBm5T7lBHRe4Dfmxy9X6hd8L3FrWxgnGV3fWZ3j0gASdYXaa+VBJlU0E2/GmCzn3T+t2mjYaeoInAnYVKVpmVMOrh6lNAeETTOHElLopblSa7TAmROq5xHIyu4a9i2qwjERTwa3Yk4Jk6q7JYVA5Cu7kS8wKVml8LdzzCTsy+elJgvH+Jf6ivpaHt/En0AJ5PZUJDev2+Y5+9j4AYfrmXfm4L73DC1ZJFJrv+Yh+EXAMPLE=",
|
||||
}
|
||||
|
||||
expectingV4 = map[string]string{
|
||||
"CanonicalHash": "41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
|
||||
"StringToSign": "AWS4-HMAC-SHA256\n20110909T233600Z\n20110909/us-east-1/iam/aws4_request\n41c56ed0df12052f7c10407a809e64cd61a4b0471956cdea28d6d1bb904f5d92",
|
||||
"SignatureV4": "08292a4b86aae1a6f80f1988182a33cbf73ccc70c5da505303e355a67cc64cb4",
|
||||
"AuthHeader": "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-amz-date, Signature=",
|
||||
}
|
||||
|
||||
requestValuesV4 = &url.Values{
|
||||
"Action": []string{"ListUsers"},
|
||||
"Version": []string{"2010-05-08"},
|
||||
}
|
||||
)
|
14
README.md
14
README.md
|
@ -93,6 +93,20 @@ Options:
|
|||
- `--azure-subscription-id`: Your Azure subscription ID.
|
||||
- `--azure-subscription-cert`: Your Azure subscription cert.
|
||||
|
||||
### Amazon EC2
|
||||
|
||||
Create machines on [Amazon Web Services](http://aws.amazon.com). You will need an Access Key ID, Secret Access Key and Subnet ID. To find the Subnet ID, login to the AWS console and go to Services -> VPC -> Subnets. Select the one where you would like to launch the instance.
|
||||
|
||||
Options:
|
||||
|
||||
- `--amazonec2-access-key`: Your access key id for the Amazon Web Services API.
|
||||
- `--amazonec2-ami`: The AMI ID of the instance to use Default: `ami-a00461c8`
|
||||
- `--amazonec2-instance-type`: The instance type to run. Default: `t2.micro`
|
||||
- `--amazonec2-region`: The region to use when launching the instance. Default: `us-east-1`
|
||||
- `--amazonec2-root-size`: The root disk size of the instance (in GB). Default: `16`
|
||||
- `--amazonec2-secret-key`: Your secret access key for the Amazon Web Services API.
|
||||
- `--amazonec2-subnet-id`: Your VPC subnet ID to launch the instance in.
|
||||
|
||||
## Contributing
|
||||
|
||||
[](https://godoc.org/github.com/docker/machine)
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"github.com/codegangsta/cli"
|
||||
|
||||
"github.com/docker/machine/drivers"
|
||||
_ "github.com/docker/machine/drivers/amazonec2"
|
||||
_ "github.com/docker/machine/drivers/azure"
|
||||
_ "github.com/docker/machine/drivers/digitalocean"
|
||||
_ "github.com/docker/machine/drivers/none"
|
||||
|
|
|
@ -0,0 +1,531 @@
|
|||
package amazonec2
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
log "github.com/Sirupsen/logrus"
|
||||
"github.com/codegangsta/cli"
|
||||
"github.com/docker/machine/drivers"
|
||||
"github.com/docker/machine/drivers/amazonec2/amz"
|
||||
"github.com/docker/machine/ssh"
|
||||
"github.com/docker/machine/state"
|
||||
)
|
||||
|
||||
const (
|
||||
driverName = "amazonec2"
|
||||
defaultRegion = "us-east-1"
|
||||
defaultAMI = "ami-a00461c8"
|
||||
defaultInstanceType = "t2.micro"
|
||||
defaultRootSize = 16
|
||||
ipRange = "0.0.0.0/0"
|
||||
)
|
||||
|
||||
type Driver struct {
|
||||
Id string
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
Region string
|
||||
AMI string
|
||||
SSHKeyID int
|
||||
KeyName string
|
||||
InstanceId string
|
||||
InstanceType string
|
||||
IPAddress string
|
||||
SubnetId string
|
||||
SecurityGroupId string
|
||||
ReservationId string
|
||||
RootSize int64
|
||||
VpcId string
|
||||
storePath string
|
||||
keyPath string
|
||||
}
|
||||
|
||||
type CreateFlags struct {
|
||||
AccessKey *string
|
||||
SecretKey *string
|
||||
Region *string
|
||||
AMI *string
|
||||
InstanceType *string
|
||||
SubnetId *string
|
||||
RootSize *int64
|
||||
}
|
||||
|
||||
func init() {
|
||||
drivers.Register(driverName, &drivers.RegisteredDriver{
|
||||
New: NewDriver,
|
||||
GetCreateFlags: GetCreateFlags,
|
||||
})
|
||||
}
|
||||
|
||||
func GetCreateFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-access-key",
|
||||
Usage: "AWS Access Key",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-secret-key",
|
||||
Usage: "AWS Secret Key",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-ami",
|
||||
Usage: "AWS machine image",
|
||||
Value: defaultAMI,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-region",
|
||||
Usage: "AWS region",
|
||||
Value: defaultRegion,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-subnet-id",
|
||||
Usage: "AWS VPC subnet id",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "amazonec2-instance-type",
|
||||
Usage: "AWS instance type",
|
||||
Value: defaultInstanceType,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "amazonec2-root-size",
|
||||
Usage: "AWS root disk size (in GB)",
|
||||
Value: defaultRootSize,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NewDriver(storePath string) (drivers.Driver, error) {
|
||||
id := generateId()
|
||||
return &Driver{Id: id, storePath: storePath}, nil
|
||||
}
|
||||
|
||||
func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
|
||||
d.AccessKey = flags.String("amazonec2-access-key")
|
||||
d.SecretKey = flags.String("amazonec2-secret-key")
|
||||
d.AMI = flags.String("amazonec2-ami")
|
||||
d.Region = flags.String("amazonec2-region")
|
||||
d.InstanceType = flags.String("amazonec2-instance-type")
|
||||
d.SubnetId = flags.String("amazonec2-subnet-id")
|
||||
d.RootSize = int64(flags.Int("amazonec2-root-size"))
|
||||
|
||||
if d.AccessKey == "" {
|
||||
return fmt.Errorf("amazonec2 driver requires the --amazonec2-access-key option")
|
||||
}
|
||||
|
||||
if d.SecretKey == "" {
|
||||
return fmt.Errorf("amazonec2 driver requires the --amazonec2-secret-key option")
|
||||
}
|
||||
|
||||
if d.SubnetId == "" {
|
||||
return fmt.Errorf("amazonec2 driver requires the --amazonec2-subnet-id option")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) DriverName() string {
|
||||
return driverName
|
||||
}
|
||||
|
||||
func (d *Driver) Create() error {
|
||||
log.Infof("Launching instance...")
|
||||
|
||||
if err := d.createKeyPair(); err != nil {
|
||||
fmt.Errorf("unable to create key pair: %s", err)
|
||||
}
|
||||
|
||||
group, err := d.createSecurityGroup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bdm := &amz.BlockDeviceMapping{
|
||||
DeviceName: "/dev/sda1",
|
||||
VolumeSize: d.RootSize,
|
||||
DeleteOnTermination: true,
|
||||
VolumeType: "gp2",
|
||||
}
|
||||
|
||||
log.Debugf("launching instance")
|
||||
instance, err := d.getClient().RunInstance(d.AMI, d.InstanceType, "a", 1, 1, group.GroupId, d.KeyName, d.SubnetId, bdm)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error launching instance: %s", err)
|
||||
}
|
||||
|
||||
d.InstanceId = instance.InstanceId
|
||||
|
||||
d.waitForInstance()
|
||||
|
||||
log.Debugf("created instance ID %s, IP address %s",
|
||||
d.InstanceId,
|
||||
d.IPAddress)
|
||||
|
||||
log.Infof("Waiting for SSH...")
|
||||
|
||||
if err := ssh.WaitForTCP(fmt.Sprintf("%s:%d", d.IPAddress, 22)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Installing Docker")
|
||||
|
||||
cmd, err := d.GetSSHCommand("if [ ! -e /usr/bin/docker ]; then curl get.docker.io | sudo sh -; fi")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err = d.GetSSHCommand("sudo stop docker")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("HACK: Downloading version of Docker with identity auth...")
|
||||
|
||||
cmd, err = d.GetSSHCommand("sudo curl -sS -o /usr/bin/docker https://bfirsh.s3.amazonaws.com/docker/docker-1.3.1-dev-identity-auth")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Debugf("Updating /etc/default/docker to use identity auth...")
|
||||
|
||||
cmd, err = d.GetSSHCommand("echo 'export DOCKER_OPTS=\"--auth=identity --host=tcp://0.0.0.0:2376 --auth-authorized-dir=/root/.docker/authorized-keys.d\"' | sudo tee -a /etc/default/docker")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// HACK: create dir for ubuntu user to access
|
||||
log.Debugf("Adding key to authorized-keys.d...")
|
||||
|
||||
cmd, err = d.GetSSHCommand("sudo mkdir -p /root/.docker && sudo chown -R ubuntu /root/.docker")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath.Join(os.Getenv("HOME"), ".docker/public-key.json"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
cmdString := fmt.Sprintf("sudo mkdir -p %q && sudo tee -a %q", "/root/.docker/authorized-keys.d", "/root/.docker/authorized-keys.d/docker-host.json")
|
||||
cmd, err = d.GetSSHCommand(cmdString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stdin = f
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd, err = d.GetSSHCommand("sudo start docker")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) GetURL() (string, error) {
|
||||
ip, err := d.GetIP()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ip == "" {
|
||||
return "", nil
|
||||
}
|
||||
return fmt.Sprintf("tcp://%s:2376", ip), nil
|
||||
}
|
||||
|
||||
func (d *Driver) GetIP() (string, error) {
|
||||
inst, err := d.getInstance()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
d.IPAddress = inst.IpAddress
|
||||
return d.IPAddress, nil
|
||||
}
|
||||
|
||||
func (d *Driver) GetState() (state.State, error) {
|
||||
inst, err := d.getInstance()
|
||||
if err != nil {
|
||||
return state.Error, err
|
||||
}
|
||||
switch inst.InstanceState.Name {
|
||||
case "pending":
|
||||
return state.Starting, nil
|
||||
case "running":
|
||||
return state.Running, nil
|
||||
case "stopping":
|
||||
return state.Stopping, nil
|
||||
case "shutting-down":
|
||||
return state.Stopping, nil
|
||||
case "stopped":
|
||||
return state.Stopped, nil
|
||||
}
|
||||
return state.None, nil
|
||||
}
|
||||
|
||||
func (d *Driver) Start() error {
|
||||
if err := d.getClient().StartInstance(d.InstanceId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.waitForInstance(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := d.updateDriver(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Stop() error {
|
||||
if err := d.getClient().StopInstance(d.InstanceId, false); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Remove() error {
|
||||
|
||||
if err := d.terminate(); err != nil {
|
||||
return fmt.Errorf("unabme to terminate instance: %s", err)
|
||||
}
|
||||
// wait until terminated so we can remove security group
|
||||
for {
|
||||
st, err := d.GetState()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if st == state.None {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if err := d.deleteSecurityGroup(); err != nil {
|
||||
return fmt.Errorf("unable to remove security group: %s", err)
|
||||
}
|
||||
|
||||
// remove keypair
|
||||
if err := d.deleteKeyPair(); err != nil {
|
||||
return fmt.Errorf("unable to remove key pair: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Restart() error {
|
||||
if err := d.getClient().RestartInstance(d.InstanceId); err != nil {
|
||||
return fmt.Errorf("unable to restart instance: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Kill() error {
|
||||
if err := d.getClient().StopInstance(d.InstanceId, true); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) Upgrade() error {
|
||||
return fmt.Errorf("unable to upgrade as we are using the custom docker binary with identity auth")
|
||||
}
|
||||
|
||||
func (d *Driver) GetSSHCommand(args ...string) (*exec.Cmd, error) {
|
||||
return ssh.GetSSHCommand(d.IPAddress, 22, "ubuntu", d.sshKeyPath(), args...), nil
|
||||
}
|
||||
|
||||
func (d *Driver) getClient() *amz.EC2 {
|
||||
auth := amz.GetAuth(d.AccessKey, d.SecretKey)
|
||||
return amz.NewEC2(auth, d.Region)
|
||||
}
|
||||
|
||||
func (d *Driver) sshKeyPath() string {
|
||||
return path.Join(d.storePath, "id_rsa")
|
||||
}
|
||||
|
||||
func (d *Driver) updateDriver() error {
|
||||
inst, err := d.getInstance()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.InstanceId = inst.InstanceId
|
||||
d.IPAddress = inst.IpAddress
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) publicSSHKeyPath() string {
|
||||
return d.sshKeyPath() + ".pub"
|
||||
}
|
||||
|
||||
func (d *Driver) getInstance() (*amz.EC2Instance, error) {
|
||||
instance, err := d.getClient().GetInstance(d.InstanceId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &instance, nil
|
||||
}
|
||||
|
||||
func (d *Driver) waitForInstance() error {
|
||||
for {
|
||||
st, err := d.GetState()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st == state.Running {
|
||||
break
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if err := d.updateDriver(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) createKeyPair() error {
|
||||
|
||||
if err := ssh.GenerateSSHKey(d.sshKeyPath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
publicKey, err := ioutil.ReadFile(d.publicSSHKeyPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
keyName := fmt.Sprintf("docker-machine-%s", d.Id)
|
||||
|
||||
log.Debugf("creating key pair: %s", keyName)
|
||||
|
||||
if err := d.getClient().ImportKeyPair(keyName, string(publicKey)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
d.KeyName = keyName
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) terminate() error {
|
||||
if d.InstanceId == "" {
|
||||
return fmt.Errorf("unknown instance")
|
||||
}
|
||||
|
||||
log.Debugf("terminating instance: %s", d.InstanceId)
|
||||
if err := d.getClient().TerminateInstance(d.InstanceId); err != nil {
|
||||
return fmt.Errorf("unable to terminate instance: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) createSecurityGroup() (*amz.SecurityGroup, error) {
|
||||
subnets, err := d.getClient().GetSubnets()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vpcId := subnets[0].VpcId
|
||||
|
||||
d.VpcId = vpcId
|
||||
|
||||
log.Debugf("creating security group in %s", d.VpcId)
|
||||
|
||||
grpName := fmt.Sprintf("docker-machine-%s", d.Id)
|
||||
group, err := d.getClient().CreateSecurityGroup(grpName, "Docker Machine", d.VpcId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.SecurityGroupId = group.GroupId
|
||||
|
||||
perms := []amz.IpPermission{
|
||||
{
|
||||
Protocol: "tcp",
|
||||
FromPort: 22,
|
||||
ToPort: 22,
|
||||
IpRange: ipRange,
|
||||
},
|
||||
{
|
||||
Protocol: "tcp",
|
||||
FromPort: 2376,
|
||||
ToPort: 2376,
|
||||
IpRange: ipRange,
|
||||
},
|
||||
}
|
||||
|
||||
log.Debugf("authorizing %s", ipRange)
|
||||
|
||||
if err := d.getClient().AuthorizeSecurityGroup(d.SecurityGroupId, perms); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (d *Driver) deleteSecurityGroup() error {
|
||||
log.Debugf("deleting security group %s", d.SecurityGroupId)
|
||||
|
||||
if err := d.getClient().DeleteSecurityGroup(d.SecurityGroupId); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Driver) deleteKeyPair() error {
|
||||
log.Debugf("deleting key pair: %s", d.KeyName)
|
||||
|
||||
if err := d.getClient().DeleteKeyPair(d.KeyName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateId() string {
|
||||
rb := make([]byte, 10)
|
||||
_, err := rand.Read(rb)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to generate id: %s", err)
|
||||
}
|
||||
|
||||
h := md5.New()
|
||||
io.WriteString(h, string(rb))
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package amz
|
||||
|
||||
type Auth struct {
|
||||
AccessKey, SecretKey string
|
||||
}
|
||||
|
||||
func GetAuth(accessKey, secretKey string) Auth {
|
||||
return Auth{accessKey, secretKey}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package amz
|
||||
|
||||
type BlockDeviceMapping struct {
|
||||
DeviceName string
|
||||
VirtualName string
|
||||
VolumeSize int64
|
||||
DeleteOnTermination bool
|
||||
VolumeType string
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package amz
|
||||
|
||||
type DescribeInstancesResponse struct {
|
||||
RequestId string `xml:"requestId"`
|
||||
ReservationSet []struct {
|
||||
InstancesSet []EC2Instance `xml:"instancesSet>item"`
|
||||
} `xml:"reservationSet>item"`
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package amz
|
||||
|
||||
type DescribeSecurityGroupsResponse struct {
|
||||
RequestId string `xml"requestId"`
|
||||
SecurityGroupInfo []struct {
|
||||
} `xml:"securityGroupInfo>item"`
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package amz
|
||||
|
||||
type DescribeSubnetsResponse struct {
|
||||
RequestId string `xml:"requestId"`
|
||||
SubnetSet []Subnet `xml:"subnetSet>item"`
|
||||
}
|
||||
|
||||
type Subnet struct {
|
||||
SubnetId string `xml:"subnetId"`
|
||||
State string `xml:"state"`
|
||||
VpcId string `xml:"vpcId"`
|
||||
CidrBlock string `xml:"cidrBlock"`
|
||||
AvailabilityZone string `xml:"availabilityZone"`
|
||||
}
|
|
@ -0,0 +1,487 @@
|
|||
package amz
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/machine/state"
|
||||
awsauth "github.com/smartystreets/go-aws-auth"
|
||||
)
|
||||
|
||||
type (
|
||||
EC2 struct {
|
||||
Endpoint string
|
||||
Auth Auth
|
||||
Region string
|
||||
}
|
||||
|
||||
Instance struct {
|
||||
info EC2Instance
|
||||
}
|
||||
|
||||
EC2Instance struct {
|
||||
InstanceId string `xml:"instanceId"`
|
||||
ImageId string `xml:"imageId"`
|
||||
InstanceState struct {
|
||||
Code int `xml:"code"`
|
||||
Name string `xml:"name"`
|
||||
} `xml:"instanceState"`
|
||||
PrivateDnsName string `xml:"privateDnsName"`
|
||||
DnsName string `xml:"dnsName"`
|
||||
Reason string `xml:"reason"`
|
||||
AmiLaunchIndex string `xml:"amiLaunchIndex"`
|
||||
ProductCodes string `xml:"productCodes"`
|
||||
InstanceType string `xml:"instanceType"`
|
||||
LaunchTime string `xml:"launchTime"`
|
||||
Placement struct {
|
||||
AvailabilityZone string `xml:"availabilityZone"`
|
||||
GroupName string `xml:"groupName"`
|
||||
Tenancy string `xml:"tenancy"`
|
||||
} `xml:"placement"`
|
||||
KernelId string `xml:"kernelId"`
|
||||
Monitoring struct {
|
||||
State string `xml:"state"`
|
||||
} `xml:"monitoring"`
|
||||
SubnetId string `xml:"subnetId"`
|
||||
VpcId string `xml:"vpcId"`
|
||||
IpAddress string `xml:"ipAddress"`
|
||||
PrivateIpAddress string `xml:"privateIpAddress"`
|
||||
SourceDestCheck bool `xml:"sourceDestCheck"`
|
||||
GroupSet []struct {
|
||||
GroupId string `xml:"groupId"`
|
||||
GroupName string `xml:"groupName"`
|
||||
} `xml:"groupSet"`
|
||||
StateReason struct {
|
||||
Code string `xml:"code"`
|
||||
Message string `xml:"message"`
|
||||
} `xml:"stateReason"`
|
||||
Architecture string `xml:"architecture"`
|
||||
RootDeviceType string `xml:"rootDeviceType"`
|
||||
RootDeviceName string `xml:"rootDeviceName"`
|
||||
BlockDeviceMapping string `xml:"blockDeviceMapping"`
|
||||
VirtualizationType string `xml:"virtualizationType"`
|
||||
ClientToken string `xml:"clientToken"`
|
||||
Hypervisor string `xml:"hypervisor"`
|
||||
NetworkInterfaceSet []struct {
|
||||
NetworkInterfaceId string `xml:"networkInterfaceId"`
|
||||
SubnetId string `xml:"subnetId"`
|
||||
VpcId string `xml:"vpcId"`
|
||||
Description string `xml:"description"`
|
||||
OwnerId string `xml:"ownerId"`
|
||||
Status string `xml:"status"`
|
||||
MacAddress string `xml:"macAddress"`
|
||||
PrivateIpAddress string `xml:"privateIpAddress"`
|
||||
PrivateDnsName string `xml:"privateDnsName"`
|
||||
SourceDestCheck string `xml:"sourceDestCheck"`
|
||||
GroupSet []struct {
|
||||
GroupId string `xml:"groupId"`
|
||||
GroupName string `xml:"groupName"`
|
||||
} `xml:"groupSet>item"`
|
||||
Attachment struct {
|
||||
AttachmentId string `xml:"attachmentId"`
|
||||
DeviceIndex string `xml:"deviceIndex"`
|
||||
Status string `xml:"status"`
|
||||
AttachTime string `xml:"attachTime"`
|
||||
DeleteOnTermination bool `xml:"deleteOnTermination"`
|
||||
} `xml:"attachment"`
|
||||
PrivateIpAddressesSet []struct {
|
||||
PrivateIpAddress string `xml:"privateIpAddress"`
|
||||
PrivateDnsName string `xml:"privateDnsName"`
|
||||
Primary bool `xml:"primary"`
|
||||
} `xml:"privateIpAddressesSet>item"`
|
||||
} `xml:"networkInterfaceSet>item"`
|
||||
EbsOptimized bool `xml:"ebsOptimized"`
|
||||
}
|
||||
|
||||
RunInstancesResponse struct {
|
||||
RequestId string `xml:"requestId"`
|
||||
ReservationId string `xml:"reservationId"`
|
||||
OwnerId string `xml:"ownerId"`
|
||||
Instances []EC2Instance `xml:"instancesSet>item"`
|
||||
}
|
||||
)
|
||||
|
||||
func newAwsApiResponseError(r http.Response) error {
|
||||
var errorResponse ErrorResponse
|
||||
if err := getDecodedResponse(r, &errorResponse); err != nil {
|
||||
return fmt.Errorf("Error decoding error response: %s", err)
|
||||
}
|
||||
msg := ""
|
||||
for _, e := range errorResponse.Errors {
|
||||
msg += fmt.Sprintf("%s\n", e.Message)
|
||||
}
|
||||
return fmt.Errorf("Non-200 API response: code=%d message=%s", r.StatusCode, msg)
|
||||
}
|
||||
|
||||
func newAwsApiCallError(err error) error {
|
||||
return fmt.Errorf("Problem with AWS API call: %s", err)
|
||||
}
|
||||
|
||||
func getDecodedResponse(r http.Response, into interface{}) error {
|
||||
defer r.Body.Close()
|
||||
if err := xml.NewDecoder(r.Body).Decode(into); err != nil {
|
||||
return fmt.Errorf("Error decoding error response: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewEC2(auth Auth, region string) *EC2 {
|
||||
endpoint := fmt.Sprintf("https://ec2.%s.amazonaws.com", region)
|
||||
return &EC2{
|
||||
Endpoint: endpoint,
|
||||
Auth: auth,
|
||||
Region: region,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EC2) awsApiCall(v url.Values) (http.Response, error) {
|
||||
v.Set("Version", "2014-06-15")
|
||||
client := &http.Client{}
|
||||
finalEndpoint := fmt.Sprintf("%s?%s", e.Endpoint, v.Encode())
|
||||
req, err := http.NewRequest("GET", finalEndpoint, nil)
|
||||
if err != nil {
|
||||
return http.Response{}, fmt.Errorf("error creating request from client")
|
||||
}
|
||||
req.Header.Add("Content-type", "application/json")
|
||||
awsauth.Sign(req, awsauth.Credentials{
|
||||
AccessKeyID: e.Auth.AccessKey,
|
||||
SecretAccessKey: e.Auth.SecretKey,
|
||||
})
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return *resp, fmt.Errorf("client encountered error while doing the request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return *resp, newAwsApiResponseError(*resp)
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func (e *EC2) RunInstance(amiId string, instanceType string, zone string, minCount int, maxCount int, securityGroup string, keyName string, subnetId string, bdm *BlockDeviceMapping) (EC2Instance, error) {
|
||||
instance := Instance{}
|
||||
v := url.Values{}
|
||||
v.Set("Action", "RunInstances")
|
||||
v.Set("ImageId", amiId)
|
||||
v.Set("Placement.AvailabilityZone", e.Region+zone)
|
||||
v.Set("MinCount", strconv.Itoa(minCount))
|
||||
v.Set("MaxCount", strconv.Itoa(maxCount))
|
||||
v.Set("KeyName", keyName)
|
||||
v.Set("InstanceType", instanceType)
|
||||
v.Set("NetworkInterface.0.DeviceIndex", "0")
|
||||
v.Set("NetworkInterface.0.SecurityGroupId.0", securityGroup)
|
||||
v.Set("NetworkInterface.0.SubnetId", subnetId)
|
||||
v.Set("NetworkInterface.0.AssociatePublicIpAddress", "1")
|
||||
|
||||
if bdm != nil {
|
||||
v.Set("BlockDeviceMapping.0.DeviceName", bdm.DeviceName)
|
||||
v.Set("BlockDeviceMapping.0.VirtualName", bdm.VirtualName)
|
||||
v.Set("BlockDeviceMapping.0.Ebs.VolumeSize", strconv.FormatInt(bdm.VolumeSize, 10))
|
||||
v.Set("BlockDeviceMapping.0.Ebs.VolumeType", bdm.VolumeType)
|
||||
deleteOnTerm := 0
|
||||
if bdm.DeleteOnTermination {
|
||||
deleteOnTerm = 1
|
||||
}
|
||||
v.Set("BlockDeviceMapping.0.Ebs.DeleteOnTermination", strconv.Itoa(deleteOnTerm))
|
||||
}
|
||||
|
||||
resp, err := e.awsApiCall(v)
|
||||
|
||||
if err != nil {
|
||||
return instance.info, newAwsApiCallError(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return instance.info, fmt.Errorf("Error reading AWS response body")
|
||||
}
|
||||
unmarshalledResponse := RunInstancesResponse{}
|
||||
err = xml.Unmarshal(contents, &unmarshalledResponse)
|
||||
if err != nil {
|
||||
return instance.info, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
instance.info = unmarshalledResponse.Instances[0]
|
||||
return instance.info, nil
|
||||
}
|
||||
|
||||
func (e *EC2) DeleteKeyPair(name string) error {
|
||||
v := url.Values{}
|
||||
v.Set("Action", "DeleteKeyPair")
|
||||
v.Set("KeyName", name)
|
||||
|
||||
_, err := e.awsApiCall(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error making API call to delete keypair :%s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) CreateKeyPair(name string) ([]byte, error) {
|
||||
v := url.Values{}
|
||||
v.Set("Action", "CreateKeyPair")
|
||||
v.Set("KeyName", name)
|
||||
resp, err := e.awsApiCall(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error trying API call to create keypair: %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error reading AWS response body")
|
||||
}
|
||||
|
||||
unmarshalledResponse := CreateKeyPairResponse{}
|
||||
if xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
|
||||
return nil, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
key := unmarshalledResponse.KeyMaterial
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (e *EC2) ImportKeyPair(name, publicKey string) error {
|
||||
keyMaterial := base64.StdEncoding.EncodeToString([]byte(publicKey))
|
||||
|
||||
v := url.Values{}
|
||||
v.Set("Action", "ImportKeyPair")
|
||||
v.Set("KeyName", name)
|
||||
v.Set("PublicKeyMaterial", keyMaterial)
|
||||
|
||||
resp, err := e.awsApiCall(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error trying API call to create keypair: %s", err)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error reading AWS response body")
|
||||
}
|
||||
|
||||
unmarshalledResponse := ImportKeyPairResponse{}
|
||||
if xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
|
||||
return fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) CreateSecurityGroup(name string, description string, vpcId string) (*SecurityGroup, error) {
|
||||
v := url.Values{}
|
||||
v.Set("Action", "CreateSecurityGroup")
|
||||
v.Set("GroupName", name)
|
||||
v.Set("GroupDescription", url.QueryEscape(description))
|
||||
v.Set("VpcId", vpcId)
|
||||
|
||||
resp, err := e.awsApiCall(v)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
// ugly hack since API has no way to check if SG already exists
|
||||
if resp.StatusCode == http.StatusBadRequest {
|
||||
var errorResponse ErrorResponse
|
||||
if err := getDecodedResponse(resp, &errorResponse); err != nil {
|
||||
return nil, fmt.Errorf("Error decoding error response: %s", err)
|
||||
}
|
||||
if errorResponse.Errors[0].Code == ErrorDuplicateGroup {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Error making API call to create security group: %s", err)
|
||||
}
|
||||
|
||||
createSecurityGroupResponse := CreateSecurityGroupResponse{}
|
||||
|
||||
if err := getDecodedResponse(resp, &createSecurityGroupResponse); err != nil {
|
||||
return nil, fmt.Errorf("Error decoding create security groups response: %s", err)
|
||||
}
|
||||
|
||||
group := &SecurityGroup{
|
||||
GroupId: createSecurityGroupResponse.GroupId,
|
||||
VpcId: vpcId,
|
||||
}
|
||||
return group, nil
|
||||
}
|
||||
|
||||
func (e *EC2) AuthorizeSecurityGroup(groupId string, permissions []IpPermission) error {
|
||||
v := url.Values{}
|
||||
v.Set("Action", "AuthorizeSecurityGroupIngress")
|
||||
v.Set("GroupId", groupId)
|
||||
|
||||
for index, perm := range permissions {
|
||||
n := index + 1 // amazon starts counting from 1 not 0
|
||||
v.Set(fmt.Sprintf("IpPermissions.%d.IpProtocol", n), perm.Protocol)
|
||||
v.Set(fmt.Sprintf("IpPermissions.%d.FromPort", n), strconv.Itoa(perm.FromPort))
|
||||
v.Set(fmt.Sprintf("IpPermissions.%d.ToPort", n), strconv.Itoa(perm.ToPort))
|
||||
v.Set(fmt.Sprintf("IpPermissions.%d.IpRanges.1.CidrIp", n), perm.IpRange)
|
||||
}
|
||||
resp, err := e.awsApiCall(v)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error making API call to authorize security group ingress: %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) DeleteSecurityGroup(groupId string) error {
|
||||
v := url.Values{}
|
||||
v.Set("Action", "DeleteSecurityGroup")
|
||||
v.Set("GroupId", groupId)
|
||||
|
||||
resp, err := e.awsApiCall(v)
|
||||
defer resp.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error making API call to delete security group: %s", err)
|
||||
}
|
||||
|
||||
deleteSecurityGroupResponse := DeleteSecurityGroupResponse{}
|
||||
|
||||
if err := getDecodedResponse(resp, &deleteSecurityGroupResponse); err != nil {
|
||||
return fmt.Errorf("Error decoding delete security groups response: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) GetSubnets() ([]Subnet, error) {
|
||||
subnets := []Subnet{}
|
||||
resp, err := e.performStandardAction("DescribeSubnets")
|
||||
if err != nil {
|
||||
return subnets, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return subnets, fmt.Errorf("Error reading AWS response body: %s", err)
|
||||
}
|
||||
|
||||
unmarshalledResponse := DescribeSubnetsResponse{}
|
||||
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
|
||||
return subnets, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
subnets = unmarshalledResponse.SubnetSet
|
||||
|
||||
return subnets, nil
|
||||
}
|
||||
func (e *EC2) GetInstanceState(instanceId string) (state.State, error) {
|
||||
resp, err := e.performInstanceAction(instanceId, "DescribeInstances", nil)
|
||||
if err != nil {
|
||||
return state.Error, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return state.Error, fmt.Errorf("Error reading AWS response body: %s", err)
|
||||
}
|
||||
|
||||
unmarshalledResponse := DescribeInstancesResponse{}
|
||||
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
|
||||
return state.Error, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
reservationSet := unmarshalledResponse.ReservationSet[0]
|
||||
instanceState := reservationSet.InstancesSet[0].InstanceState
|
||||
|
||||
shortState := strings.TrimSpace(instanceState.Name)
|
||||
switch shortState {
|
||||
case "pending":
|
||||
return state.Starting, nil
|
||||
case "running":
|
||||
return state.Running, nil
|
||||
case "stopped":
|
||||
return state.Stopped, nil
|
||||
case "stopping":
|
||||
return state.Stopped, nil
|
||||
}
|
||||
|
||||
return state.Error, nil
|
||||
}
|
||||
|
||||
func (e *EC2) GetInstance(instanceId string) (EC2Instance, error) {
|
||||
ec2Instance := EC2Instance{}
|
||||
resp, err := e.performInstanceAction(instanceId, "DescribeInstances", nil)
|
||||
if err != nil {
|
||||
return ec2Instance, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
contents, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ec2Instance, fmt.Errorf("Error reading AWS response body: %s", err)
|
||||
}
|
||||
|
||||
unmarshalledResponse := DescribeInstancesResponse{}
|
||||
if err = xml.Unmarshal(contents, &unmarshalledResponse); err != nil {
|
||||
return ec2Instance, fmt.Errorf("Error unmarshalling AWS response XML: %s", err)
|
||||
}
|
||||
|
||||
reservationSet := unmarshalledResponse.ReservationSet[0]
|
||||
instance := reservationSet.InstancesSet[0]
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (e *EC2) StartInstance(instanceId string) error {
|
||||
if _, err := e.performInstanceAction(instanceId, "StartInstances", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) RestartInstance(instanceId string) error {
|
||||
if _, err := e.performInstanceAction(instanceId, "RebootInstances", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) StopInstance(instanceId string, force bool) error {
|
||||
vars := make(map[string]string)
|
||||
if force {
|
||||
vars["Force"] = "1"
|
||||
}
|
||||
|
||||
if _, err := e.performInstanceAction(instanceId, "StopInstances", &vars); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) TerminateInstance(instanceId string) error {
|
||||
if _, err := e.performInstanceAction(instanceId, "TerminateInstances", nil); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *EC2) performStandardAction(action string) (http.Response, error) {
|
||||
v := url.Values{}
|
||||
v.Set("Action", action)
|
||||
resp, err := e.awsApiCall(v)
|
||||
if err != nil {
|
||||
return resp, newAwsApiCallError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (e *EC2) performInstanceAction(instanceId, action string, extraVars *map[string]string) (http.Response, error) {
|
||||
v := url.Values{}
|
||||
v.Set("Action", action)
|
||||
v.Set("InstanceId.1", instanceId)
|
||||
if extraVars != nil {
|
||||
for k, val := range *extraVars {
|
||||
v.Set(k, val)
|
||||
}
|
||||
}
|
||||
resp, err := e.awsApiCall(v)
|
||||
if err != nil {
|
||||
return resp, newAwsApiCallError(err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package amz
|
||||
|
||||
type ErrorResponse struct {
|
||||
Errors []struct {
|
||||
Code string
|
||||
Message string
|
||||
} `xml:"Errors>Error"`
|
||||
RequestID string
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package amz
|
||||
|
||||
const (
|
||||
ErrorDuplicateGroup = "InvalidGroup.Duplicate"
|
||||
)
|
|
@ -0,0 +1,8 @@
|
|||
package amz
|
||||
|
||||
type IpPermission struct {
|
||||
Protocol string
|
||||
FromPort int
|
||||
ToPort int
|
||||
IpRange string
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package amz
|
||||
|
||||
type CreateKeyPairResponse struct {
|
||||
KeyName string `xml:"keyName"`
|
||||
KeyFingerprint string `xml:"keyFingerprint"`
|
||||
KeyMaterial []byte `xml:"keyMaterial"`
|
||||
}
|
||||
|
||||
type ImportKeyPairResponse struct {
|
||||
KeyName string `xml:"keyName"`
|
||||
KeyFingerprint string `xml:"keyFingerprint"`
|
||||
KeyMaterial []byte `xml:"keyMaterial"`
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package amz
|
||||
|
||||
type CreateSecurityGroupResponse struct {
|
||||
RequestId string `xml:"requestId"`
|
||||
Return bool `xml:"return"`
|
||||
GroupId string `xml:"groupId"`
|
||||
}
|
||||
|
||||
type DeleteSecurityGroupResponse struct {
|
||||
RequestId string `xml:"requestId"`
|
||||
Return bool `xml:"return"`
|
||||
}
|
||||
|
||||
type SecurityGroup struct {
|
||||
GroupId string
|
||||
VpcId string
|
||||
}
|
|
@ -9,6 +9,7 @@ const (
|
|||
Paused
|
||||
Saved
|
||||
Stopped
|
||||
Stopping
|
||||
Starting
|
||||
Error
|
||||
)
|
||||
|
@ -19,6 +20,7 @@ var states = []string{
|
|||
"Paused",
|
||||
"Saved",
|
||||
"Stopped",
|
||||
"Stopping",
|
||||
"Starting",
|
||||
"Error",
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue