diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 275c30f450..ec14f0ea82 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -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" diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/LICENSE b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/LICENSE new file mode 100644 index 0000000000..0071511a96 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/LICENSE @@ -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. diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/README.md b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/README.md new file mode 100644 index 0000000000..e2f6e405e6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/README.md @@ -0,0 +1,86 @@ +go-aws-auth +=========== + +[![GoDoc](https://godoc.org/github.com/smartystreets/go-aws-auth?status.svg)](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! diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth.go new file mode 100644 index 0000000000..7bc9228535 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth.go @@ -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 +) diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth_test.go new file mode 100644 index 0000000000..4ff5dbe38f --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/awsauth_test.go @@ -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{} diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common.go new file mode 100644 index 0000000000..f39c899175 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common.go @@ -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) +} diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common_test.go new file mode 100644 index 0000000000..0ae891c3b8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/common_test.go @@ -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") + }) +} diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3.go new file mode 100644 index 0000000000..bca79a2c98 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3.go @@ -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" +) diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3_test.go new file mode 100644 index 0000000000..39c4e179a9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/s3_test.go @@ -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" +) diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2.go new file mode 100644 index 0000000000..c10f132f11 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2.go @@ -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" diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2_test.go new file mode 100644 index 0000000000..c69557d03b --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign2_test.go @@ -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" +) diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3.go new file mode 100644 index 0000000000..5a1260d9cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3.go @@ -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 diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3_test.go new file mode 100644 index 0000000000..f4e87fd28c --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign3_test.go @@ -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=" +) diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4.go new file mode 100644 index 0000000000..3ae9da6831 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4.go @@ -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" diff --git a/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4_test.go b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4_test.go new file mode 100644 index 0000000000..d0924d3e84 --- /dev/null +++ b/Godeps/_workspace/src/github.com/smartystreets/go-aws-auth/sign4_test.go @@ -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"}, + } +) diff --git a/README.md b/README.md index a925c0b3ee..d43b1a7597 100644 --- a/README.md +++ b/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 [![GoDoc](https://godoc.org/github.com/docker/machine?status.png)](https://godoc.org/github.com/docker/machine) diff --git a/commands.go b/commands.go index 14c285350d..6e079db9b4 100644 --- a/commands.go +++ b/commands.go @@ -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" diff --git a/drivers/amazonec2/amazonec2.go b/drivers/amazonec2/amazonec2.go new file mode 100644 index 0000000000..984d8357cd --- /dev/null +++ b/drivers/amazonec2/amazonec2.go @@ -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)) +} diff --git a/drivers/amazonec2/amz/auth.go b/drivers/amazonec2/amz/auth.go new file mode 100644 index 0000000000..c307bc4fcb --- /dev/null +++ b/drivers/amazonec2/amz/auth.go @@ -0,0 +1,9 @@ +package amz + +type Auth struct { + AccessKey, SecretKey string +} + +func GetAuth(accessKey, secretKey string) Auth { + return Auth{accessKey, secretKey} +} diff --git a/drivers/amazonec2/amz/block_device_mapping.go b/drivers/amazonec2/amz/block_device_mapping.go new file mode 100644 index 0000000000..27c6cecf3d --- /dev/null +++ b/drivers/amazonec2/amz/block_device_mapping.go @@ -0,0 +1,9 @@ +package amz + +type BlockDeviceMapping struct { + DeviceName string + VirtualName string + VolumeSize int64 + DeleteOnTermination bool + VolumeType string +} diff --git a/drivers/amazonec2/amz/describe_instances.go b/drivers/amazonec2/amz/describe_instances.go new file mode 100644 index 0000000000..34faa78bce --- /dev/null +++ b/drivers/amazonec2/amz/describe_instances.go @@ -0,0 +1,8 @@ +package amz + +type DescribeInstancesResponse struct { + RequestId string `xml:"requestId"` + ReservationSet []struct { + InstancesSet []EC2Instance `xml:"instancesSet>item"` + } `xml:"reservationSet>item"` +} diff --git a/drivers/amazonec2/amz/describe_security_groups.go b/drivers/amazonec2/amz/describe_security_groups.go new file mode 100644 index 0000000000..981db6af8a --- /dev/null +++ b/drivers/amazonec2/amz/describe_security_groups.go @@ -0,0 +1,7 @@ +package amz + +type DescribeSecurityGroupsResponse struct { + RequestId string `xml"requestId"` + SecurityGroupInfo []struct { + } `xml:"securityGroupInfo>item"` +} diff --git a/drivers/amazonec2/amz/describe_subnets.go b/drivers/amazonec2/amz/describe_subnets.go new file mode 100644 index 0000000000..1fabdc5c05 --- /dev/null +++ b/drivers/amazonec2/amz/describe_subnets.go @@ -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"` +} diff --git a/drivers/amazonec2/amz/ec2.go b/drivers/amazonec2/amz/ec2.go new file mode 100644 index 0000000000..33cd34b3d6 --- /dev/null +++ b/drivers/amazonec2/amz/ec2.go @@ -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 +} diff --git a/drivers/amazonec2/amz/error.go b/drivers/amazonec2/amz/error.go new file mode 100644 index 0000000000..b8cd98b828 --- /dev/null +++ b/drivers/amazonec2/amz/error.go @@ -0,0 +1,9 @@ +package amz + +type ErrorResponse struct { + Errors []struct { + Code string + Message string + } `xml:"Errors>Error"` + RequestID string +} diff --git a/drivers/amazonec2/amz/error_codes.go b/drivers/amazonec2/amz/error_codes.go new file mode 100644 index 0000000000..a4de953b4e --- /dev/null +++ b/drivers/amazonec2/amz/error_codes.go @@ -0,0 +1,5 @@ +package amz + +const ( + ErrorDuplicateGroup = "InvalidGroup.Duplicate" +) diff --git a/drivers/amazonec2/amz/ip_permission.go b/drivers/amazonec2/amz/ip_permission.go new file mode 100644 index 0000000000..325de3b699 --- /dev/null +++ b/drivers/amazonec2/amz/ip_permission.go @@ -0,0 +1,8 @@ +package amz + +type IpPermission struct { + Protocol string + FromPort int + ToPort int + IpRange string +} diff --git a/drivers/amazonec2/amz/keypair.go b/drivers/amazonec2/amz/keypair.go new file mode 100644 index 0000000000..faebe250f2 --- /dev/null +++ b/drivers/amazonec2/amz/keypair.go @@ -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"` +} diff --git a/drivers/amazonec2/amz/security_group.go b/drivers/amazonec2/amz/security_group.go new file mode 100644 index 0000000000..018dfba26c --- /dev/null +++ b/drivers/amazonec2/amz/security_group.go @@ -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 +} diff --git a/state/state.go b/state/state.go index 9c7e8275e4..655cb4f8c7 100644 --- a/state/state.go +++ b/state/state.go @@ -9,6 +9,7 @@ const ( Paused Saved Stopped + Stopping Starting Error ) @@ -19,6 +20,7 @@ var states = []string{ "Paused", "Saved", "Stopped", + "Stopping", "Starting", "Error", }