77e69b9cf3
Signed-off-by: Olivier Gambier <olivier@docker.com>
636 lines
16 KiB
Go
636 lines
16 KiB
Go
//
|
|
// goamz - Go packages to interact with the Amazon Web Services.
|
|
//
|
|
// https://wiki.ubuntu.com/goamz
|
|
//
|
|
// Copyright (c) 2011 Canonical Ltd.
|
|
//
|
|
// Written by Gustavo Niemeyer <gustavo.niemeyer@canonical.com>
|
|
//
|
|
package aws
|
|
|
|
import (
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/user"
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Regular expressions for INI files
|
|
var (
|
|
iniSectionRegexp = regexp.MustCompile(`^\s*\[([^\[\]]+)\]\s*$`)
|
|
iniSettingRegexp = regexp.MustCompile(`^\s*(.+?)\s*=\s*(.*\S)\s*$`)
|
|
)
|
|
|
|
// Defines the valid signers
|
|
const (
|
|
V2Signature = iota
|
|
V4Signature = iota
|
|
Route53Signature = iota
|
|
)
|
|
|
|
// Defines the service endpoint and correct Signer implementation to use
|
|
// to sign requests for this endpoint
|
|
type ServiceInfo struct {
|
|
Endpoint string
|
|
Signer uint
|
|
}
|
|
|
|
// Region defines the URLs where AWS services may be accessed.
|
|
//
|
|
// See http://goo.gl/d8BP1 for more details.
|
|
type Region struct {
|
|
Name string // the canonical name of this region.
|
|
EC2Endpoint ServiceInfo
|
|
S3Endpoint string
|
|
S3BucketEndpoint string // Not needed by AWS S3. Use ${bucket} for bucket name.
|
|
S3LocationConstraint bool // true if this region requires a LocationConstraint declaration.
|
|
S3LowercaseBucket bool // true if the region requires bucket names to be lower case.
|
|
SDBEndpoint string
|
|
SNSEndpoint string
|
|
SQSEndpoint string
|
|
SESEndpoint string
|
|
IAMEndpoint string
|
|
ELBEndpoint string
|
|
KMSEndpoint string
|
|
DynamoDBEndpoint string
|
|
CloudWatchServicepoint ServiceInfo
|
|
AutoScalingEndpoint string
|
|
RDSEndpoint ServiceInfo
|
|
KinesisEndpoint string
|
|
STSEndpoint string
|
|
CloudFormationEndpoint string
|
|
ElastiCacheEndpoint string
|
|
}
|
|
|
|
var Regions = map[string]Region{
|
|
APNortheast.Name: APNortheast,
|
|
APNortheast2.Name: APNortheast2,
|
|
APSoutheast.Name: APSoutheast,
|
|
APSoutheast2.Name: APSoutheast2,
|
|
EUCentral.Name: EUCentral,
|
|
EUWest.Name: EUWest,
|
|
USEast.Name: USEast,
|
|
USWest.Name: USWest,
|
|
USWest2.Name: USWest2,
|
|
USGovWest.Name: USGovWest,
|
|
SAEast.Name: SAEast,
|
|
CNNorth1.Name: CNNorth1,
|
|
}
|
|
|
|
// Designates a signer interface suitable for signing AWS requests, params
|
|
// should be appropriately encoded for the request before signing.
|
|
//
|
|
// A signer should be initialized with Auth and the appropriate endpoint.
|
|
type Signer interface {
|
|
Sign(method, path string, params map[string]string)
|
|
}
|
|
|
|
// An AWS Service interface with the API to query the AWS service
|
|
//
|
|
// Supplied as an easy way to mock out service calls during testing.
|
|
type AWSService interface {
|
|
// Queries the AWS service at a given method/path with the params and
|
|
// returns an http.Response and error
|
|
Query(method, path string, params map[string]string) (*http.Response, error)
|
|
// Builds an error given an XML payload in the http.Response, can be used
|
|
// to process an error if the status code is not 200 for example.
|
|
BuildError(r *http.Response) error
|
|
}
|
|
|
|
// Implements a Server Query/Post API to easily query AWS services and build
|
|
// errors when desired
|
|
type Service struct {
|
|
service ServiceInfo
|
|
signer Signer
|
|
}
|
|
|
|
// Create a base set of params for an action
|
|
func MakeParams(action string) map[string]string {
|
|
params := make(map[string]string)
|
|
params["Action"] = action
|
|
return params
|
|
}
|
|
|
|
// Create a new AWS server to handle making requests
|
|
func NewService(auth Auth, service ServiceInfo) (s *Service, err error) {
|
|
var signer Signer
|
|
switch service.Signer {
|
|
case V2Signature:
|
|
signer, err = NewV2Signer(auth, service)
|
|
// case V4Signature:
|
|
// signer, err = NewV4Signer(auth, service, Regions["eu-west-1"])
|
|
default:
|
|
err = fmt.Errorf("Unsupported signer for service")
|
|
}
|
|
if err != nil {
|
|
return
|
|
}
|
|
s = &Service{service: service, signer: signer}
|
|
return
|
|
}
|
|
|
|
func (s *Service) Query(method, path string, params map[string]string) (resp *http.Response, err error) {
|
|
params["Timestamp"] = time.Now().UTC().Format(time.RFC3339)
|
|
u, err := url.Parse(s.service.Endpoint)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
u.Path = path
|
|
|
|
s.signer.Sign(method, path, params)
|
|
if method == "GET" {
|
|
u.RawQuery = multimap(params).Encode()
|
|
resp, err = http.Get(u.String())
|
|
} else if method == "POST" {
|
|
resp, err = http.PostForm(u.String(), multimap(params))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Service) BuildError(r *http.Response) error {
|
|
errors := ErrorResponse{}
|
|
xml.NewDecoder(r.Body).Decode(&errors)
|
|
var err Error
|
|
err = errors.Errors
|
|
err.RequestId = errors.RequestId
|
|
err.StatusCode = r.StatusCode
|
|
if err.Message == "" {
|
|
err.Message = r.Status
|
|
}
|
|
return &err
|
|
}
|
|
|
|
type ServiceError interface {
|
|
error
|
|
ErrorCode() string
|
|
}
|
|
|
|
type ErrorResponse struct {
|
|
Errors Error `xml:"Error"`
|
|
RequestId string // A unique ID for tracking the request
|
|
}
|
|
|
|
type Error struct {
|
|
StatusCode int
|
|
Type string
|
|
Code string
|
|
Message string
|
|
RequestId string
|
|
}
|
|
|
|
func (err *Error) Error() string {
|
|
return fmt.Sprintf("Type: %s, Code: %s, Message: %s",
|
|
err.Type, err.Code, err.Message,
|
|
)
|
|
}
|
|
|
|
func (err *Error) ErrorCode() string {
|
|
return err.Code
|
|
}
|
|
|
|
type Auth struct {
|
|
AccessKey, SecretKey string
|
|
token string
|
|
expiration time.Time
|
|
}
|
|
|
|
func (a *Auth) Token() string {
|
|
if a.token == "" {
|
|
return ""
|
|
}
|
|
if time.Since(a.expiration) >= -30*time.Second { //in an ideal world this should be zero assuming the instance is synching it's clock
|
|
auth, err := GetAuth("", "", "", time.Time{})
|
|
if err == nil {
|
|
*a = auth
|
|
}
|
|
}
|
|
return a.token
|
|
}
|
|
|
|
func (a *Auth) Expiration() time.Time {
|
|
return a.expiration
|
|
}
|
|
|
|
// To be used with other APIs that return auth credentials such as STS
|
|
func NewAuth(accessKey, secretKey, token string, expiration time.Time) *Auth {
|
|
return &Auth{
|
|
AccessKey: accessKey,
|
|
SecretKey: secretKey,
|
|
token: token,
|
|
expiration: expiration,
|
|
}
|
|
}
|
|
|
|
// ResponseMetadata
|
|
type ResponseMetadata struct {
|
|
RequestId string // A unique ID for tracking the request
|
|
}
|
|
|
|
type BaseResponse struct {
|
|
ResponseMetadata ResponseMetadata
|
|
}
|
|
|
|
var unreserved = make([]bool, 128)
|
|
var hex = "0123456789ABCDEF"
|
|
|
|
func init() {
|
|
// RFC3986
|
|
u := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890-_.~"
|
|
for _, c := range u {
|
|
unreserved[c] = true
|
|
}
|
|
}
|
|
|
|
func multimap(p map[string]string) url.Values {
|
|
q := make(url.Values, len(p))
|
|
for k, v := range p {
|
|
q[k] = []string{v}
|
|
}
|
|
return q
|
|
}
|
|
|
|
type credentials struct {
|
|
Code string
|
|
LastUpdated string
|
|
Type string
|
|
AccessKeyId string
|
|
SecretAccessKey string
|
|
Token string
|
|
Expiration string
|
|
}
|
|
|
|
// GetMetaData retrieves instance metadata about the current machine.
|
|
//
|
|
// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html for more details.
|
|
func GetMetaData(path string) (contents []byte, err error) {
|
|
c := http.Client{
|
|
Transport: &http.Transport{
|
|
Dial: func(netw, addr string) (net.Conn, error) {
|
|
deadline := time.Now().Add(5 * time.Second)
|
|
c, err := net.DialTimeout(netw, addr, time.Second*2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
c.SetDeadline(deadline)
|
|
return c, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
url := "http://169.254.169.254/latest/meta-data/" + path
|
|
|
|
resp, err := c.Get(url)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
err = fmt.Errorf("Code %d returned for url %s", resp.StatusCode, url)
|
|
return
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return
|
|
}
|
|
return []byte(body), err
|
|
}
|
|
|
|
func GetRegion(regionName string) (region Region) {
|
|
region = Regions[regionName]
|
|
return
|
|
}
|
|
|
|
// GetInstanceCredentials creates an Auth based on the instance's role credentials.
|
|
// If the running instance is not in EC2 or does not have a valid IAM role, an error will be returned.
|
|
// For more info about setting up IAM roles, see http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html
|
|
func GetInstanceCredentials() (cred credentials, err error) {
|
|
credentialPath := "iam/security-credentials/"
|
|
|
|
// Get the instance role
|
|
role, err := GetMetaData(credentialPath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Get the instance role credentials
|
|
credentialJSON, err := GetMetaData(credentialPath + string(role))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(credentialJSON), &cred)
|
|
return
|
|
}
|
|
|
|
// GetAuth creates an Auth based on either passed in credentials,
|
|
// environment information or instance based role credentials.
|
|
func GetAuth(accessKey string, secretKey, token string, expiration time.Time) (auth Auth, err error) {
|
|
// First try passed in credentials
|
|
if accessKey != "" && secretKey != "" {
|
|
return Auth{accessKey, secretKey, token, expiration}, nil
|
|
}
|
|
|
|
// Next try to get auth from the environment
|
|
auth, err = EnvAuth()
|
|
if err == nil {
|
|
// Found auth, return
|
|
return
|
|
}
|
|
|
|
// Next try getting auth from the instance role
|
|
cred, err := GetInstanceCredentials()
|
|
if err == nil {
|
|
// Found auth, return
|
|
auth.AccessKey = cred.AccessKeyId
|
|
auth.SecretKey = cred.SecretAccessKey
|
|
auth.token = cred.Token
|
|
exptdate, err := time.Parse("2006-01-02T15:04:05Z", cred.Expiration)
|
|
if err != nil {
|
|
err = fmt.Errorf("Error Parsing expiration date: cred.Expiration :%s , error: %s \n", cred.Expiration, err)
|
|
}
|
|
auth.expiration = exptdate
|
|
return auth, err
|
|
}
|
|
|
|
// Next try getting auth from the credentials file
|
|
auth, err = CredentialFileAuth("", "", time.Minute*5)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
//err = errors.New("No valid AWS authentication found")
|
|
err = fmt.Errorf("No valid AWS authentication found: %s", err)
|
|
return auth, err
|
|
}
|
|
|
|
// EnvAuth creates an Auth based on environment information.
|
|
// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
|
|
// variables are used.
|
|
func EnvAuth() (auth Auth, err error) {
|
|
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
|
|
if auth.AccessKey == "" {
|
|
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY")
|
|
}
|
|
|
|
auth.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
|
if auth.SecretKey == "" {
|
|
auth.SecretKey = os.Getenv("AWS_SECRET_KEY")
|
|
}
|
|
if auth.AccessKey == "" {
|
|
err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
|
|
}
|
|
if auth.SecretKey == "" {
|
|
err = errors.New("AWS_SECRET_ACCESS_KEY or AWS_SECRET_KEY not found in environment")
|
|
}
|
|
return
|
|
}
|
|
|
|
// CredentialFileAuth creates and Auth based on a credentials file. The file
|
|
// contains various authentication profiles for use with AWS.
|
|
//
|
|
// The credentials file, which is used by other AWS SDKs, is documented at
|
|
// http://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs
|
|
func CredentialFileAuth(filePath string, profile string, expiration time.Duration) (auth Auth, err error) {
|
|
if profile == "" {
|
|
profile = os.Getenv("AWS_DEFAULT_PROFILE")
|
|
if profile == "" {
|
|
profile = os.Getenv("AWS_PROFILE")
|
|
if profile == "" {
|
|
profile = "default"
|
|
}
|
|
}
|
|
}
|
|
|
|
if filePath == "" {
|
|
u, err := user.Current()
|
|
if err != nil {
|
|
return auth, err
|
|
}
|
|
|
|
filePath = path.Join(u.HomeDir, ".aws", "credentials")
|
|
}
|
|
|
|
// read the file, then parse the INI
|
|
contents, err := ioutil.ReadFile(filePath)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
profiles := parseINI(string(contents))
|
|
profileData, ok := profiles[profile]
|
|
|
|
if !ok {
|
|
err = errors.New("The credentials file did not contain the profile")
|
|
return
|
|
}
|
|
|
|
keyId, ok := profileData["aws_access_key_id"]
|
|
if !ok {
|
|
err = errors.New("The credentials file did not contain required attribute aws_access_key_id")
|
|
return
|
|
}
|
|
|
|
secretKey, ok := profileData["aws_secret_access_key"]
|
|
if !ok {
|
|
err = errors.New("The credentials file did not contain required attribute aws_secret_access_key")
|
|
return
|
|
}
|
|
|
|
auth.AccessKey = keyId
|
|
auth.SecretKey = secretKey
|
|
|
|
if token, ok := profileData["aws_session_token"]; ok {
|
|
auth.token = token
|
|
}
|
|
|
|
auth.expiration = time.Now().Add(expiration)
|
|
|
|
return
|
|
}
|
|
|
|
// parseINI takes the contents of a credentials file and returns a map, whose keys
|
|
// are the various profiles, and whose values are maps of the settings for the
|
|
// profiles
|
|
func parseINI(fileContents string) map[string]map[string]string {
|
|
profiles := make(map[string]map[string]string)
|
|
|
|
lines := strings.Split(fileContents, "\n")
|
|
|
|
var currentSection map[string]string
|
|
for _, line := range lines {
|
|
// remove comments, which start with a semi-colon
|
|
if split := strings.Split(line, ";"); len(split) > 1 {
|
|
line = split[0]
|
|
}
|
|
|
|
// check if the line is the start of a profile.
|
|
//
|
|
// for example:
|
|
// [default]
|
|
//
|
|
// otherwise, check for the proper setting
|
|
// property=value
|
|
if sectMatch := iniSectionRegexp.FindStringSubmatch(line); len(sectMatch) == 2 {
|
|
currentSection = make(map[string]string)
|
|
profiles[sectMatch[1]] = currentSection
|
|
} else if setMatch := iniSettingRegexp.FindStringSubmatch(line); len(setMatch) == 3 && currentSection != nil {
|
|
currentSection[setMatch[1]] = setMatch[2]
|
|
}
|
|
}
|
|
|
|
return profiles
|
|
}
|
|
|
|
// Encode takes a string and URI-encodes it in a way suitable
|
|
// to be used in AWS signatures.
|
|
func Encode(s string) string {
|
|
encode := false
|
|
for i := 0; i != len(s); i++ {
|
|
c := s[i]
|
|
if c > 127 || !unreserved[c] {
|
|
encode = true
|
|
break
|
|
}
|
|
}
|
|
if !encode {
|
|
return s
|
|
}
|
|
e := make([]byte, len(s)*3)
|
|
ei := 0
|
|
for i := 0; i != len(s); i++ {
|
|
c := s[i]
|
|
if c > 127 || !unreserved[c] {
|
|
e[ei] = '%'
|
|
e[ei+1] = hex[c>>4]
|
|
e[ei+2] = hex[c&0xF]
|
|
ei += 3
|
|
} else {
|
|
e[ei] = c
|
|
ei += 1
|
|
}
|
|
}
|
|
return string(e[:ei])
|
|
}
|
|
|
|
func dialTimeout(network, addr string) (net.Conn, error) {
|
|
return net.DialTimeout(network, addr, time.Duration(2*time.Second))
|
|
}
|
|
|
|
func AvailabilityZone() string {
|
|
transport := http.Transport{Dial: dialTimeout}
|
|
client := http.Client{
|
|
Transport: &transport,
|
|
}
|
|
resp, err := client.Get("http://169.254.169.254/latest/meta-data/placement/availability-zone")
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
return string(body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func InstanceRegion() string {
|
|
az := AvailabilityZone()
|
|
if az == "unknown" {
|
|
return az
|
|
} else {
|
|
region := az[:len(az)-1]
|
|
return region
|
|
}
|
|
}
|
|
|
|
func InstanceId() string {
|
|
transport := http.Transport{Dial: dialTimeout}
|
|
client := http.Client{
|
|
Transport: &transport,
|
|
}
|
|
resp, err := client.Get("http://169.254.169.254/latest/meta-data/instance-id")
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
return string(body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func InstanceType() string {
|
|
transport := http.Transport{Dial: dialTimeout}
|
|
client := http.Client{
|
|
Transport: &transport,
|
|
}
|
|
resp, err := client.Get("http://169.254.169.254/latest/meta-data/instance-type")
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "unknown"
|
|
} else {
|
|
return string(body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ServerLocalIp() string {
|
|
transport := http.Transport{Dial: dialTimeout}
|
|
client := http.Client{
|
|
Transport: &transport,
|
|
}
|
|
resp, err := client.Get("http://169.254.169.254/latest/meta-data/local-ipv4")
|
|
if err != nil {
|
|
return "127.0.0.1"
|
|
} else {
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "127.0.0.1"
|
|
} else {
|
|
return string(body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func ServerPublicIp() string {
|
|
transport := http.Transport{Dial: dialTimeout}
|
|
client := http.Client{
|
|
Transport: &transport,
|
|
}
|
|
resp, err := client.Get("http://169.254.169.254/latest/meta-data/public-ipv4")
|
|
if err != nil {
|
|
return "127.0.0.1"
|
|
} else {
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "127.0.0.1"
|
|
} else {
|
|
return string(body)
|
|
}
|
|
}
|
|
}
|