feature (s3): add ec2 role base authentication

This commit is contained in:
Mickael Kerjean 2021-09-12 17:12:58 +10:00
parent 43937cab32
commit 3214cfd588
533 changed files with 69898 additions and 98378 deletions

4
go.mod
View file

@ -3,7 +3,7 @@ module github.com/mickael-kerjean/filestash
go 1.13
require (
github.com/aws/aws-sdk-go v1.28.0
github.com/aws/aws-sdk-go v1.40.41
github.com/cretz/bine v0.1.0
github.com/go-sql-driver/mysql v1.5.0
github.com/gorilla/mux v1.7.3
@ -23,7 +23,7 @@ require (
github.com/tidwall/sjson v1.0.4
golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de
golang.org/x/image v0.0.0-20210622092929-e6eecd499c2c
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
google.golang.org/api v0.15.0

14
go.sum
View file

@ -11,6 +11,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aws/aws-sdk-go v1.28.0 h1:NkmnHFVEMTRYTleRLm5xUaL1mHKKkYQl4rCd+jzD58c=
github.com/aws/aws-sdk-go v1.28.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.40.41 h1:v/Y4bB8+wHCONtKV+fuHTzLiqC08lk8e9HqYhRB9PBQ=
github.com/aws/aws-sdk-go v1.40.41/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
@ -59,6 +61,9 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY=
github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
@ -84,6 +89,8 @@ github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTK
github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.11.0 h1:4Zv0OGbpkg4yNuUtH0s8rvoYxRCNyT29NVUo6pgPmxI=
github.com/pkg/sftp v1.11.0/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -139,6 +146,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -158,6 +167,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e h1:D5TXcfTk7xF7hvieo4QErS3qqCB4teTffacDWr7CI+0=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@ -206,6 +219,7 @@ gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQb
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -2,18 +2,19 @@ package plg_backend_s3
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/s3"
"github.com/aws/aws-sdk-go/service/s3/s3manager"
. "github.com/mickael-kerjean/filestash/server/common"
"io"
"os"
"path/filepath"
"strings"
)
var S3Cache AppCache
@ -38,13 +39,24 @@ func (s S3Backend) Init(params map[string]string, app *App) (IBackend, error) {
params["region"] = "us-east-2"
}
config := &aws.Config{
Credentials: credentials.NewStaticCredentials(params["access_key_id"], params["secret_access_key"], params["session_token"]),
Credentials: credentials.NewChainCredentials(
[]credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: params["access_key_id"],
SecretAccessKey: params["secret_access_key"],
SessionToken: params["session_token"],
}},
&credentials.EnvProvider{},
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(session.Must(session.NewSession()))},
},
),
S3ForcePathStyle: aws.Bool(true),
Region: aws.String(params["region"]),
}
if params["endpoint"] != "" {
config.Endpoint = aws.String(params["endpoint"])
}
backend := &S3Backend{
config: config,
params: params,
@ -170,7 +182,6 @@ func (s S3Backend) Ls(path string) (files []os.FileInfo, err error) {
}
return true
})
return files, err
}
@ -284,10 +295,10 @@ func (s S3Backend) Mv(from string, to string) error {
t := s.path(to)
client := s3.New(s.createSession(f.bucket))
if (f.path == "") {
if f.path == "" {
// Rename bucket
return ErrNotImplemented
} else if (strings.HasSuffix(from, "/") == false) {
} else if strings.HasSuffix(from, "/") == false {
// Move Single file
input := &s3.CopyObjectInput{
Bucket: aws.String(t.bucket),
@ -300,7 +311,7 @@ func (s S3Backend) Mv(from string, to string) error {
input.SSECustomerAlgorithm = aws.String("AES256")
input.SSECustomerKey = aws.String(s.params["encryption_key"])
}
_, err := client.CopyObject(input)
if err != nil {
return err
@ -335,17 +346,17 @@ func (s S3Backend) Mv(from string, to string) error {
input.SSECustomerKey = aws.String(s.params["encryption_key"])
}
Log.Debug("CopyObject(%s, %s):", from, f.bucket + "/" + toKey)
Log.Debug("CopyObject(%s, %s):", from, f.bucket+"/"+toKey)
_, err := client.CopyObject(input)
if err != nil {
Log.Error("CopyObject from: %s to: %s",
f.bucket + "/" + *obj.Key,
t.bucket + "/" + t.path + *obj.Key,
f.bucket+"/"+*obj.Key,
t.bucket+"/"+t.path+*obj.Key,
err)
return false
}
Log.Debug("DeleteObject(%s):", f.bucket + "/" + *obj.Key)
Log.Debug("DeleteObject(%s):", f.bucket+"/"+*obj.Key)
_, err = client.DeleteObject(&s3.DeleteObjectInput{
Bucket: aws.String(f.bucket),
Key: obj.Key,

View file

@ -50,9 +50,19 @@ func prettify(v reflect.Value, indent int, buf *bytes.Buffer) {
for i, n := range names {
val := v.FieldByName(n)
ft, ok := v.Type().FieldByName(n)
if !ok {
panic(fmt.Sprintf("expected to find field %v on type %v, but was not found", n, v.Type()))
}
buf.WriteString(strings.Repeat(" ", indent+2))
buf.WriteString(n + ": ")
prettify(val, indent+2, buf)
if tag := ft.Tag.Get("sensitive"); tag == "true" {
buf.WriteString("<sensitive>")
} else {
prettify(val, indent+2, buf)
}
if i < len(names)-1 {
buf.WriteString(",\n")

View file

@ -8,6 +8,8 @@ import (
)
// StringValue returns the string representation of a value.
//
// Deprecated: Use Prettify instead.
func StringValue(i interface{}) string {
var buf bytes.Buffer
stringValue(reflect.ValueOf(i), 0, &buf)

View file

@ -88,10 +88,6 @@ func (c *Client) NewRequest(operation *request.Operation, params interface{}, da
// AddDebugHandlers injects debug logging handlers into the service to log request
// debug information.
func (c *Client) AddDebugHandlers() {
if !c.Config.LogLevel.AtLeast(aws.LogDebug) {
return
}
c.Handlers.Send.PushFrontNamed(LogHTTPRequestHandler)
c.Handlers.Send.PushBackNamed(LogHTTPResponseHandler)
}

View file

@ -53,6 +53,10 @@ var LogHTTPRequestHandler = request.NamedHandler{
}
func logRequest(r *request.Request) {
if !r.Config.LogLevel.AtLeast(aws.LogDebug) {
return
}
logBody := r.Config.LogLevel.Matches(aws.LogDebugWithHTTPBody)
bodySeekable := aws.IsReaderSeekable(r.Body)
@ -120,6 +124,10 @@ var LogHTTPResponseHandler = request.NamedHandler{
}
func logResponse(r *request.Request) {
if !r.Config.LogLevel.AtLeast(aws.LogDebug) {
return
}
lw := &logWriter{r.Config.Logger, bytes.NewBuffer(nil)}
if r.HTTPResponse == nil {

View file

@ -43,7 +43,7 @@ type Config struct {
// An optional endpoint URL (hostname only or fully qualified URI)
// that overrides the default generated endpoint for a client. Set this
// to `""` to use the default generated endpoint.
// to `nil` or the value to `""` to use the default generated endpoint.
//
// Note: You must still provide a `Region` value when specifying an
// endpoint for a client.
@ -138,7 +138,7 @@ type Config struct {
// `ExpectContinueTimeout` for information on adjusting the continue wait
// timeout. https://golang.org/pkg/net/http/#Transport
//
// You should use this flag to disble 100-Continue if you experience issues
// You should use this flag to disable 100-Continue if you experience issues
// with proxies or third party S3 compatible services.
S3Disable100Continue *bool
@ -183,7 +183,7 @@ type Config struct {
//
// Example:
// sess := session.Must(session.NewSession(aws.NewConfig()
// .WithEC2MetadataDiableTimeoutOverride(true)))
// .WithEC2MetadataDisableTimeoutOverride(true)))
//
// svc := s3.New(sess)
//
@ -194,7 +194,7 @@ type Config struct {
// both IPv4 and IPv6 addressing.
//
// Setting this for a service which does not support dual stack will fail
// to make requets. It is not recommended to set this value on the session
// to make requests. It is not recommended to set this value on the session
// as it will apply to all service clients created with the session. Even
// services which don't support dual stack endpoints.
//
@ -238,6 +238,7 @@ type Config struct {
// EnableEndpointDiscovery will allow for endpoint discovery on operations that
// have the definition in its model. By default, endpoint discovery is off.
// To use EndpointDiscovery, Endpoint should be unset or set to an empty string.
//
// Example:
// sess := session.Must(session.NewSession(&aws.Config{
@ -437,13 +438,6 @@ func (c *Config) WithDisableEndpointHostPrefix(t bool) *Config {
return c
}
// MergeIn merges the passed in configs into the existing config object.
func (c *Config) MergeIn(cfgs ...*Config) {
for _, other := range cfgs {
mergeInConfig(c, other)
}
}
// WithSTSRegionalEndpoint will set whether or not to use regional endpoint flag
// when resolving the endpoint for a service
func (c *Config) WithSTSRegionalEndpoint(sre endpoints.STSRegionalEndpoint) *Config {
@ -458,6 +452,27 @@ func (c *Config) WithS3UsEast1RegionalEndpoint(sre endpoints.S3UsEast1RegionalEn
return c
}
// WithLowerCaseHeaderMaps sets a config LowerCaseHeaderMaps value
// returning a Config pointer for chaining.
func (c *Config) WithLowerCaseHeaderMaps(t bool) *Config {
c.LowerCaseHeaderMaps = &t
return c
}
// WithDisableRestProtocolURICleaning sets a config DisableRestProtocolURICleaning value
// returning a Config pointer for chaining.
func (c *Config) WithDisableRestProtocolURICleaning(t bool) *Config {
c.DisableRestProtocolURICleaning = &t
return c
}
// MergeIn merges the passed in configs into the existing config object.
func (c *Config) MergeIn(cfgs ...*Config) {
for _, other := range cfgs {
mergeInConfig(c, other)
}
}
func mergeInConfig(dst *Config, other *Config) {
if other == nil {
return
@ -570,6 +585,10 @@ func mergeInConfig(dst *Config, other *Config) {
if other.S3UsEast1RegionalEndpoint != endpoints.UnsetS3UsEast1Endpoint {
dst.S3UsEast1RegionalEndpoint = other.S3UsEast1RegionalEndpoint
}
if other.LowerCaseHeaderMaps != nil {
dst.LowerCaseHeaderMaps = other.LowerCaseHeaderMaps
}
}
// Copy will return a shallow copy of the Config object. If any additional

View file

@ -1,3 +1,4 @@
//go:build !go1.9
// +build !go1.9
package aws

View file

@ -1,3 +1,4 @@
//go:build go1.9
// +build go1.9
package aws

View file

@ -1,43 +1,10 @@
//go:build !go1.7
// +build !go1.7
package aws
import "time"
// An emptyCtx is a copy of the Go 1.7 context.emptyCtx type. This is copied to
// provide a 1.6 and 1.5 safe version of context that is compatible with Go
// 1.7's Context.
//
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case backgroundCtx:
return "aws.BackgroundContext"
}
return "unknown empty Context"
}
var (
backgroundCtx = new(emptyCtx)
import (
"github.com/aws/aws-sdk-go/internal/context"
)
// BackgroundContext returns a context that will never be canceled, has no
@ -52,5 +19,5 @@ var (
//
// See https://golang.org/pkg/context for more information on Contexts.
func BackgroundContext() Context {
return backgroundCtx
return context.BackgroundCtx
}

View file

@ -1,3 +1,4 @@
//go:build go1.7
// +build go1.7
package aws

View file

@ -178,7 +178,7 @@ func handleSendError(r *request.Request, err error) {
var ValidateResponseHandler = request.NamedHandler{Name: "core.ValidateResponseHandler", Fn: func(r *request.Request) {
if r.HTTPResponse.StatusCode == 0 || r.HTTPResponse.StatusCode >= 300 {
// this may be replaced by an UnmarshalError handler
r.Error = awserr.New("UnknownError", "unknown error", nil)
r.Error = awserr.New("UnknownError", "unknown error", r.Error)
}
}}
@ -225,6 +225,8 @@ var ValidateEndpointHandler = request.NamedHandler{Name: "core.ValidateEndpointH
if r.ClientInfo.SigningRegion == "" && aws.StringValue(r.Config.Region) == "" {
r.Error = aws.ErrMissingRegion
} else if r.ClientInfo.Endpoint == "" {
// Was any endpoint provided by the user, or one was derived by the
// SDK's endpoint resolver?
r.Error = aws.ErrMissingEndpoint
}
}}

View file

@ -0,0 +1,23 @@
//go:build !go1.7
// +build !go1.7
package credentials
import (
"github.com/aws/aws-sdk-go/internal/context"
)
// backgroundContext returns a context that will never be canceled, has no
// values, and no deadline. This context is used by the SDK to provide
// backwards compatibility with non-context API operations and functionality.
//
// Go 1.6 and before:
// This context function is equivalent to context.Background in the Go stdlib.
//
// Go 1.7 and later:
// The context returned will be the value returned by context.Background()
//
// See https://golang.org/pkg/context for more information on Contexts.
func backgroundContext() Context {
return context.BackgroundCtx
}

View file

@ -0,0 +1,21 @@
//go:build go1.7
// +build go1.7
package credentials
import "context"
// backgroundContext returns a context that will never be canceled, has no
// values, and no deadline. This context is used by the SDK to provide
// backwards compatibility with non-context API operations and functionality.
//
// Go 1.6 and before:
// This context function is equivalent to context.Background in the Go stdlib.
//
// Go 1.7 and later:
// The context returned will be the value returned by context.Background()
//
// See https://golang.org/pkg/context for more information on Contexts.
func backgroundContext() Context {
return context.Background()
}

View file

@ -0,0 +1,40 @@
//go:build !go1.9
// +build !go1.9
package credentials
import "time"
// Context is an copy of the Go v1.7 stdlib's context.Context interface.
// It is represented as a SDK interface to enable you to use the "WithContext"
// API methods with Go v1.6 and a Context type such as golang.org/x/net/context.
//
// This type, aws.Context, and context.Context are equivalent.
//
// See https://golang.org/pkg/context on how to use contexts.
type Context interface {
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
Done() <-chan struct{}
// Err returns a non-nil error value after Done is closed. Err returns
// Canceled if the context was canceled or DeadlineExceeded if the
// context's deadline passed. No other values for Err are defined.
// After Done is closed, successive calls to Err return the same value.
Err() error
// Value returns the value associated with this context for key, or nil
// if no value is associated with key. Successive calls to Value with
// the same key returns the same result.
//
// Use context values only for request-scoped data that transits
// processes and API boundaries, not for passing optional parameters to
// functions.
Value(key interface{}) interface{}
}

View file

@ -0,0 +1,14 @@
//go:build go1.9
// +build go1.9
package credentials
import "context"
// Context is an alias of the Go stdlib's context.Context interface.
// It can be used within the SDK's API operation "WithContext" methods.
//
// This type, aws.Context, and context.Context are equivalent.
//
// See https://golang.org/pkg/context on how to use contexts.
type Context = context.Context

View file

@ -54,6 +54,7 @@ import (
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/internal/sync/singleflight"
)
// AnonymousCredentials is an empty Credential object that can be used as
@ -106,6 +107,13 @@ type Provider interface {
IsExpired() bool
}
// ProviderWithContext is a Provider that can retrieve credentials with a Context
type ProviderWithContext interface {
Provider
RetrieveWithContext(Context) (Value, error)
}
// An Expirer is an interface that Providers can implement to expose the expiration
// time, if known. If the Provider cannot accurately provide this info,
// it should not implement this interface.
@ -165,7 +173,9 @@ type Expiry struct {
// the expiration time given to ensure no requests are made with expired
// tokens.
func (e *Expiry) SetExpiration(expiration time.Time, window time.Duration) {
e.expiration = expiration
// Passed in expirations should have the monotonic clock values stripped.
// This ensures time comparisons will be based on wall-time.
e.expiration = expiration.Round(0)
if window > 0 {
e.expiration = e.expiration.Add(-window)
}
@ -197,20 +207,82 @@ func (e *Expiry) ExpiresAt() time.Time {
// first instance of the credentials Value. All calls to Get() after that
// will return the cached credentials Value until IsExpired() returns true.
type Credentials struct {
creds Value
forceRefresh bool
m sync.RWMutex
sf singleflight.Group
m sync.RWMutex
creds Value
provider Provider
}
// NewCredentials returns a pointer to a new Credentials with the provider set.
func NewCredentials(provider Provider) *Credentials {
return &Credentials{
provider: provider,
forceRefresh: true,
c := &Credentials{
provider: provider,
}
return c
}
// GetWithContext returns the credentials value, or error if the credentials
// Value failed to be retrieved. Will return early if the passed in context is
// canceled.
//
// Will return the cached credentials Value if it has not expired. If the
// credentials Value has expired the Provider's Retrieve() will be called
// to refresh the credentials.
//
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
//
// Passed in Context is equivalent to aws.Context, and context.Context.
func (c *Credentials) GetWithContext(ctx Context) (Value, error) {
// Check if credentials are cached, and not expired.
select {
case curCreds, ok := <-c.asyncIsExpired():
// ok will only be true, of the credentials were not expired. ok will
// be false and have no value if the credentials are expired.
if ok {
return curCreds, nil
}
case <-ctx.Done():
return Value{}, awserr.New("RequestCanceled",
"request context canceled", ctx.Err())
}
// Cannot pass context down to the actual retrieve, because the first
// context would cancel the whole group when there is not direct
// association of items in the group.
resCh := c.sf.DoChan("", func() (interface{}, error) {
return c.singleRetrieve(&suppressedContext{ctx})
})
select {
case res := <-resCh:
return res.Val.(Value), res.Err
case <-ctx.Done():
return Value{}, awserr.New("RequestCanceled",
"request context canceled", ctx.Err())
}
}
func (c *Credentials) singleRetrieve(ctx Context) (interface{}, error) {
c.m.Lock()
defer c.m.Unlock()
if curCreds := c.creds; !c.isExpiredLocked(curCreds) {
return curCreds, nil
}
var creds Value
var err error
if p, ok := c.provider.(ProviderWithContext); ok {
creds, err = p.RetrieveWithContext(ctx)
} else {
creds, err = c.provider.Retrieve()
}
if err == nil {
c.creds = creds
}
return creds, err
}
// Get returns the credentials value, or error if the credentials Value failed
@ -223,30 +295,7 @@ func NewCredentials(provider Provider) *Credentials {
// If Credentials.Expire() was called the credentials Value will be force
// expired, and the next call to Get() will cause them to be refreshed.
func (c *Credentials) Get() (Value, error) {
// Check the cached credentials first with just the read lock.
c.m.RLock()
if !c.isExpired() {
creds := c.creds
c.m.RUnlock()
return creds, nil
}
c.m.RUnlock()
// Credentials are expired need to retrieve the credentials taking the full
// lock.
c.m.Lock()
defer c.m.Unlock()
if c.isExpired() {
creds, err := c.provider.Retrieve()
if err != nil {
return Value{}, err
}
c.creds = creds
c.forceRefresh = false
}
return c.creds, nil
return c.GetWithContext(backgroundContext())
}
// Expire expires the credentials and forces them to be retrieved on the
@ -258,7 +307,7 @@ func (c *Credentials) Expire() {
c.m.Lock()
defer c.m.Unlock()
c.forceRefresh = true
c.creds = Value{}
}
// IsExpired returns if the credentials are no longer valid, and need
@ -270,12 +319,30 @@ func (c *Credentials) IsExpired() bool {
c.m.RLock()
defer c.m.RUnlock()
return c.isExpired()
return c.isExpiredLocked(c.creds)
}
// isExpired helper method wrapping the definition of expired credentials.
func (c *Credentials) isExpired() bool {
return c.forceRefresh || c.provider.IsExpired()
// asyncIsExpired returns a channel of credentials Value. If the channel is
// closed the credentials are expired and credentials value are not empty.
func (c *Credentials) asyncIsExpired() <-chan Value {
ch := make(chan Value, 1)
go func() {
c.m.RLock()
defer c.m.RUnlock()
if curCreds := c.creds; !c.isExpiredLocked(curCreds) {
ch <- curCreds
}
close(ch)
}()
return ch
}
// isExpiredLocked helper method wrapping the definition of expired credentials.
func (c *Credentials) isExpiredLocked(creds interface{}) bool {
return creds == nil || creds.(Value) == Value{} || c.provider.IsExpired()
}
// ExpiresAt provides access to the functionality of the Expirer interface of
@ -288,12 +355,29 @@ func (c *Credentials) ExpiresAt() (time.Time, error) {
expirer, ok := c.provider.(Expirer)
if !ok {
return time.Time{}, awserr.New("ProviderNotExpirer",
fmt.Sprintf("provider %s does not support ExpiresAt()", c.creds.ProviderName),
fmt.Sprintf("provider %s does not support ExpiresAt()",
c.creds.ProviderName),
nil)
}
if c.forceRefresh {
if c.creds == (Value{}) {
// set expiration time to the distant past
return time.Time{}, nil
}
return expirer.ExpiresAt(), nil
}
type suppressedContext struct {
Context
}
func (s *suppressedContext) Deadline() (deadline time.Time, ok bool) {
return time.Time{}, false
}
func (s *suppressedContext) Done() <-chan struct{} {
return nil
}
func (s *suppressedContext) Err() error {
return nil
}

View file

@ -7,6 +7,7 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
@ -87,7 +88,14 @@ func NewCredentialsWithClient(client *ec2metadata.EC2Metadata, options ...func(*
// Error will be returned if the request fails, or unable to extract
// the desired credentials.
func (m *EC2RoleProvider) Retrieve() (credentials.Value, error) {
credsList, err := requestCredList(m.Client)
return m.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext retrieves credentials from the EC2 service.
// Error will be returned if the request fails, or unable to extract
// the desired credentials.
func (m *EC2RoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
credsList, err := requestCredList(ctx, m.Client)
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}
@ -97,7 +105,7 @@ func (m *EC2RoleProvider) Retrieve() (credentials.Value, error) {
}
credsName := credsList[0]
roleCreds, err := requestCred(m.Client, credsName)
roleCreds, err := requestCred(ctx, m.Client, credsName)
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}
@ -130,8 +138,8 @@ const iamSecurityCredsPath = "iam/security-credentials/"
// requestCredList requests a list of credentials from the EC2 service.
// If there are no credentials, or there is an error making or receiving the request
func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
resp, err := client.GetMetadata(iamSecurityCredsPath)
func requestCredList(ctx aws.Context, client *ec2metadata.EC2Metadata) ([]string, error) {
resp, err := client.GetMetadataWithContext(ctx, iamSecurityCredsPath)
if err != nil {
return nil, awserr.New("EC2RoleRequestError", "no EC2 instance role found", err)
}
@ -154,8 +162,8 @@ func requestCredList(client *ec2metadata.EC2Metadata) ([]string, error) {
//
// If the credentials cannot be found, or there is an error reading the response
// and error will be returned.
func requestCred(client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
resp, err := client.GetMetadata(sdkuri.PathJoin(iamSecurityCredsPath, credsName))
func requestCred(ctx aws.Context, client *ec2metadata.EC2Metadata, credsName string) (ec2RoleCredRespBody, error) {
resp, err := client.GetMetadataWithContext(ctx, sdkuri.PathJoin(iamSecurityCredsPath, credsName))
if err != nil {
return ec2RoleCredRespBody{},
awserr.New("EC2RoleRequestError",

View file

@ -116,7 +116,13 @@ func (p *Provider) IsExpired() bool {
// Retrieve will attempt to request the credentials from the endpoint the Provider
// was configured for. And error will be returned if the retrieval fails.
func (p *Provider) Retrieve() (credentials.Value, error) {
resp, err := p.getCredentials()
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext will attempt to request the credentials from the endpoint the Provider
// was configured for. And error will be returned if the retrieval fails.
func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
resp, err := p.getCredentials(ctx)
if err != nil {
return credentials.Value{ProviderName: ProviderName},
awserr.New("CredentialsEndpointError", "failed to load credentials", err)
@ -148,7 +154,7 @@ type errorOutput struct {
Message string `json:"message"`
}
func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
func (p *Provider) getCredentials(ctx aws.Context) (*getCredentialsOutput, error) {
op := &request.Operation{
Name: "GetCredentials",
HTTPMethod: "GET",
@ -156,6 +162,7 @@ func (p *Provider) getCredentials() (*getCredentialsOutput, error) {
out := &getCredentialsOutput{}
req := p.Client.NewRequest(op, nil, out)
req.SetContext(ctx)
req.HTTPRequest.Header.Set("Accept", "application/json")
if authToken := p.AuthorizationToken; len(authToken) != 0 {
req.HTTPRequest.Header.Set("Authorization", authToken)

View file

@ -17,8 +17,9 @@ var (
ErrSharedCredentialsHomeNotFound = awserr.New("UserHomeNotFound", "user home directory not found.", nil)
)
// A SharedCredentialsProvider retrieves credentials from the current user's home
// directory, and keeps track if those credentials are expired.
// A SharedCredentialsProvider retrieves access key pair (access key ID,
// secret access key, and session token if present) credentials from the current
// user's home directory, and keeps track if those credentials are expired.
//
// Profile ini file example: $HOME/.aws/credentials
type SharedCredentialsProvider struct {

View file

@ -0,0 +1,60 @@
// Package ssocreds provides a credential provider for retrieving temporary AWS credentials using an SSO access token.
//
// IMPORTANT: The provider in this package does not initiate or perform the AWS SSO login flow. The SDK provider
// expects that you have already performed the SSO login flow using AWS CLI using the "aws sso login" command, or by
// some other mechanism. The provider must find a valid non-expired access token for the AWS SSO user portal URL in
// ~/.aws/sso/cache. If a cached token is not found, it is expired, or the file is malformed an error will be returned.
//
// Loading AWS SSO credentials with the AWS shared configuration file
//
// You can use configure AWS SSO credentials from the AWS shared configuration file by
// providing the specifying the required keys in the profile:
//
// sso_account_id
// sso_region
// sso_role_name
// sso_start_url
//
// For example, the following defines a profile "devsso" and specifies the AWS SSO parameters that defines the target
// account, role, sign-on portal, and the region where the user portal is located. Note: all SSO arguments must be
// provided, or an error will be returned.
//
// [profile devsso]
// sso_start_url = https://my-sso-portal.awsapps.com/start
// sso_role_name = SSOReadOnlyRole
// sso_region = us-east-1
// sso_account_id = 123456789012
//
// Using the config module, you can load the AWS SDK shared configuration, and specify that this profile be used to
// retrieve credentials. For example:
//
// sess, err := session.NewSessionWithOptions(session.Options{
// SharedConfigState: session.SharedConfigEnable,
// Profile: "devsso",
// })
// if err != nil {
// return err
// }
//
// Programmatically loading AWS SSO credentials directly
//
// You can programmatically construct the AWS SSO Provider in your application, and provide the necessary information
// to load and retrieve temporary credentials using an access token from ~/.aws/sso/cache.
//
// svc := sso.New(sess, &aws.Config{
// Region: aws.String("us-west-2"), // Client Region must correspond to the AWS SSO user portal region
// })
//
// provider := ssocreds.NewCredentialsWithClient(svc, "123456789012", "SSOReadOnlyRole", "https://my-sso-portal.awsapps.com/start")
//
// credentials, err := provider.Get()
// if err != nil {
// return err
// }
//
// Additional Resources
//
// Configuring the AWS CLI to use AWS Single Sign-On: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html
//
// AWS Single Sign-On User Guide: https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html
package ssocreds

View file

@ -0,0 +1,10 @@
//go:build !windows
// +build !windows
package ssocreds
import "os"
func getHomeDirectory() string {
return os.Getenv("HOME")
}

View file

@ -0,0 +1,7 @@
package ssocreds
import "os"
func getHomeDirectory() string {
return os.Getenv("USERPROFILE")
}

View file

@ -0,0 +1,180 @@
package ssocreds
import (
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/service/sso"
"github.com/aws/aws-sdk-go/service/sso/ssoiface"
)
// ErrCodeSSOProviderInvalidToken is the code type that is returned if loaded token has expired or is otherwise invalid.
// To refresh the SSO session run aws sso login with the corresponding profile.
const ErrCodeSSOProviderInvalidToken = "SSOProviderInvalidToken"
const invalidTokenMessage = "the SSO session has expired or is invalid"
func init() {
nowTime = time.Now
defaultCacheLocation = defaultCacheLocationImpl
}
var nowTime func() time.Time
// ProviderName is the name of the provider used to specify the source of credentials.
const ProviderName = "SSOProvider"
var defaultCacheLocation func() string
func defaultCacheLocationImpl() string {
return filepath.Join(getHomeDirectory(), ".aws", "sso", "cache")
}
// Provider is an AWS credential provider that retrieves temporary AWS credentials by exchanging an SSO login token.
type Provider struct {
credentials.Expiry
// The Client which is configured for the AWS Region where the AWS SSO user portal is located.
Client ssoiface.SSOAPI
// The AWS account that is assigned to the user.
AccountID string
// The role name that is assigned to the user.
RoleName string
// The URL that points to the organization's AWS Single Sign-On (AWS SSO) user portal.
StartURL string
}
// NewCredentials returns a new AWS Single Sign-On (AWS SSO) credential provider. The ConfigProvider is expected to be configured
// for the AWS Region where the AWS SSO user portal is located.
func NewCredentials(configProvider client.ConfigProvider, accountID, roleName, startURL string, optFns ...func(provider *Provider)) *credentials.Credentials {
return NewCredentialsWithClient(sso.New(configProvider), accountID, roleName, startURL, optFns...)
}
// NewCredentialsWithClient returns a new AWS Single Sign-On (AWS SSO) credential provider. The provided client is expected to be configured
// for the AWS Region where the AWS SSO user portal is located.
func NewCredentialsWithClient(client ssoiface.SSOAPI, accountID, roleName, startURL string, optFns ...func(provider *Provider)) *credentials.Credentials {
p := &Provider{
Client: client,
AccountID: accountID,
RoleName: roleName,
StartURL: startURL,
}
for _, fn := range optFns {
fn(p)
}
return credentials.NewCredentials(p)
}
// Retrieve retrieves temporary AWS credentials from the configured Amazon Single Sign-On (AWS SSO) user portal
// by exchanging the accessToken present in ~/.aws/sso/cache.
func (p *Provider) Retrieve() (credentials.Value, error) {
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext retrieves temporary AWS credentials from the configured Amazon Single Sign-On (AWS SSO) user portal
// by exchanging the accessToken present in ~/.aws/sso/cache.
func (p *Provider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
tokenFile, err := loadTokenFile(p.StartURL)
if err != nil {
return credentials.Value{}, err
}
output, err := p.Client.GetRoleCredentialsWithContext(ctx, &sso.GetRoleCredentialsInput{
AccessToken: &tokenFile.AccessToken,
AccountId: &p.AccountID,
RoleName: &p.RoleName,
})
if err != nil {
return credentials.Value{}, err
}
expireTime := time.Unix(0, aws.Int64Value(output.RoleCredentials.Expiration)*int64(time.Millisecond)).UTC()
p.SetExpiration(expireTime, 0)
return credentials.Value{
AccessKeyID: aws.StringValue(output.RoleCredentials.AccessKeyId),
SecretAccessKey: aws.StringValue(output.RoleCredentials.SecretAccessKey),
SessionToken: aws.StringValue(output.RoleCredentials.SessionToken),
ProviderName: ProviderName,
}, nil
}
func getCacheFileName(url string) (string, error) {
hash := sha1.New()
_, err := hash.Write([]byte(url))
if err != nil {
return "", err
}
return strings.ToLower(hex.EncodeToString(hash.Sum(nil))) + ".json", nil
}
type rfc3339 time.Time
func (r *rfc3339) UnmarshalJSON(bytes []byte) error {
var value string
if err := json.Unmarshal(bytes, &value); err != nil {
return err
}
parse, err := time.Parse(time.RFC3339, value)
if err != nil {
return fmt.Errorf("expected RFC3339 timestamp: %v", err)
}
*r = rfc3339(parse)
return nil
}
type token struct {
AccessToken string `json:"accessToken"`
ExpiresAt rfc3339 `json:"expiresAt"`
Region string `json:"region,omitempty"`
StartURL string `json:"startUrl,omitempty"`
}
func (t token) Expired() bool {
return nowTime().Round(0).After(time.Time(t.ExpiresAt))
}
func loadTokenFile(startURL string) (t token, err error) {
key, err := getCacheFileName(startURL)
if err != nil {
return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, err)
}
fileBytes, err := ioutil.ReadFile(filepath.Join(defaultCacheLocation(), key))
if err != nil {
return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, err)
}
if err := json.Unmarshal(fileBytes, &t); err != nil {
return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, err)
}
if len(t.AccessToken) == 0 {
return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, nil)
}
if t.Expired() {
return token{}, awserr.New(ErrCodeSSOProviderInvalidToken, invalidTokenMessage, nil)
}
return t, nil
}

View file

@ -19,7 +19,9 @@ type StaticProvider struct {
}
// NewStaticCredentials returns a pointer to a new Credentials object
// wrapping a static credentials value provider.
// wrapping a static credentials value provider. Token is only required
// for temporary security credentials retrieved via STS, otherwise an empty
// string can be passed for this parameter.
func NewStaticCredentials(id, secret, token string) *Credentials {
return NewCredentials(&StaticProvider{Value: Value{
AccessKeyID: id,

View file

@ -87,6 +87,7 @@ import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/sdkrand"
"github.com/aws/aws-sdk-go/service/sts"
)
@ -94,7 +95,7 @@ import (
// StdinTokenProvider will prompt on stderr and read from stdin for a string value.
// An error is returned if reading from stdin fails.
//
// Use this function go read MFA tokens from stdin. The function makes no attempt
// Use this function to read MFA tokens from stdin. The function makes no attempt
// to make atomic prompts from stdin across multiple gorouties.
//
// Using StdinTokenProvider with multiple AssumeRoleProviders, or Credentials will
@ -118,6 +119,10 @@ type AssumeRoler interface {
AssumeRole(input *sts.AssumeRoleInput) (*sts.AssumeRoleOutput, error)
}
type assumeRolerWithContext interface {
AssumeRoleWithContext(aws.Context, *sts.AssumeRoleInput, ...request.Option) (*sts.AssumeRoleOutput, error)
}
// DefaultDuration is the default amount of time in minutes that the credentials
// will be valid for.
var DefaultDuration = time.Duration(15) * time.Minute
@ -164,6 +169,29 @@ type AssumeRoleProvider struct {
// size.
Policy *string
// The ARNs of IAM managed policies you want to use as managed session policies.
// The policies must exist in the same account as the role.
//
// This parameter is optional. You can provide up to 10 managed policy ARNs.
// However, the plain text that you use for both inline and managed session
// policies can't exceed 2,048 characters.
//
// An AWS conversion compresses the passed session policies and session tags
// into a packed binary format that has a separate limit. Your request can fail
// for this limit even if your plain text meets the other requirements. The
// PackedPolicySize response element indicates by percentage how close the policies
// and tags for your request are to the upper size limit.
//
// Passing policies to this operation returns new temporary credentials. The
// resulting session's permissions are the intersection of the role's identity-based
// policy and the session policies. You can use the role's temporary credentials
// in subsequent AWS API calls to access resources in the account that owns
// the role. You cannot use session policies to grant more permissions than
// those allowed by the identity-based policy of the role that is being assumed.
// For more information, see Session Policies (https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#policies_session)
// in the IAM User Guide.
PolicyArns []*sts.PolicyDescriptorType
// The identification number of the MFA device that is associated with the user
// who is making the AssumeRole call. Specify this value if the trust policy
// of the role being assumed includes a condition that requires MFA authentication.
@ -216,9 +244,11 @@ type AssumeRoleProvider struct {
MaxJitterFrac float64
}
// NewCredentials returns a pointer to a new Credentials object wrapping the
// NewCredentials returns a pointer to a new Credentials value wrapping the
// AssumeRoleProvider. The credentials will expire every 15 minutes and the
// role will be named after a nanosecond timestamp of this operation.
// role will be named after a nanosecond timestamp of this operation. The
// Credentials value will attempt to refresh the credentials using the provider
// when Credentials.Get is called, if the cached credentials are expiring.
//
// Takes a Config provider to create the STS client. The ConfigProvider is
// satisfied by the session.Session type.
@ -240,9 +270,11 @@ func NewCredentials(c client.ConfigProvider, roleARN string, options ...func(*As
return credentials.NewCredentials(p)
}
// NewCredentialsWithClient returns a pointer to a new Credentials object wrapping the
// NewCredentialsWithClient returns a pointer to a new Credentials value wrapping the
// AssumeRoleProvider. The credentials will expire every 15 minutes and the
// role will be named after a nanosecond timestamp of this operation.
// role will be named after a nanosecond timestamp of this operation. The
// Credentials value will attempt to refresh the credentials using the provider
// when Credentials.Get is called, if the cached credentials are expiring.
//
// Takes an AssumeRoler which can be satisfied by the STS client.
//
@ -265,6 +297,11 @@ func NewCredentialsWithClient(svc AssumeRoler, roleARN string, options ...func(*
// Retrieve generates a new set of temporary credentials using STS.
func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext generates a new set of temporary credentials using STS.
func (p *AssumeRoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
// Apply defaults where parameters are not set.
if p.RoleSessionName == "" {
// Try to work out a role name that will hopefully end up unique.
@ -281,6 +318,7 @@ func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
RoleSessionName: aws.String(p.RoleSessionName),
ExternalId: p.ExternalID,
Tags: p.Tags,
PolicyArns: p.PolicyArns,
TransitiveTagKeys: p.TransitiveTagKeys,
}
if p.Policy != nil {
@ -304,7 +342,15 @@ func (p *AssumeRoleProvider) Retrieve() (credentials.Value, error) {
}
}
roleOutput, err := p.Client.AssumeRole(input)
var roleOutput *sts.AssumeRoleOutput
var err error
if c, ok := p.Client.(assumeRolerWithContext); ok {
roleOutput, err = c.AssumeRoleWithContext(ctx, input)
} else {
roleOutput, err = p.Client.AssumeRole(input)
}
if err != nil {
return credentials.Value{ProviderName: ProviderName}, err
}

View file

@ -28,15 +28,46 @@ const (
// compare test values.
var now = time.Now
// TokenFetcher shuold return WebIdentity token bytes or an error
type TokenFetcher interface {
FetchToken(credentials.Context) ([]byte, error)
}
// FetchTokenPath is a path to a WebIdentity token file
type FetchTokenPath string
// FetchToken returns a token by reading from the filesystem
func (f FetchTokenPath) FetchToken(ctx credentials.Context) ([]byte, error) {
data, err := ioutil.ReadFile(string(f))
if err != nil {
errMsg := fmt.Sprintf("unable to read file at %s", f)
return nil, awserr.New(ErrCodeWebIdentity, errMsg, err)
}
return data, nil
}
// WebIdentityRoleProvider is used to retrieve credentials using
// an OIDC token.
type WebIdentityRoleProvider struct {
credentials.Expiry
PolicyArns []*sts.PolicyDescriptorType
client stsiface.STSAPI
// Duration the STS credentials will be valid for. Truncated to seconds.
// If unset, the assumed role will use AssumeRoleWithWebIdentity's default
// expiry duration. See
// https://docs.aws.amazon.com/sdk-for-go/api/service/sts/#STS.AssumeRoleWithWebIdentity
// for more information.
Duration time.Duration
// The amount of time the credentials will be refreshed before they expire.
// This is useful refresh credentials before they expire to reduce risk of
// using credentials as they expire. If unset, will default to no expiry
// window.
ExpiryWindow time.Duration
tokenFilePath string
client stsiface.STSAPI
tokenFetcher TokenFetcher
roleARN string
roleSessionName string
}
@ -52,9 +83,15 @@ func NewWebIdentityCredentials(c client.ConfigProvider, roleARN, roleSessionName
// NewWebIdentityRoleProvider will return a new WebIdentityRoleProvider with the
// provided stsiface.STSAPI
func NewWebIdentityRoleProvider(svc stsiface.STSAPI, roleARN, roleSessionName, path string) *WebIdentityRoleProvider {
return NewWebIdentityRoleProviderWithToken(svc, roleARN, roleSessionName, FetchTokenPath(path))
}
// NewWebIdentityRoleProviderWithToken will return a new WebIdentityRoleProvider with the
// provided stsiface.STSAPI and a TokenFetcher
func NewWebIdentityRoleProviderWithToken(svc stsiface.STSAPI, roleARN, roleSessionName string, tokenFetcher TokenFetcher) *WebIdentityRoleProvider {
return &WebIdentityRoleProvider{
client: svc,
tokenFilePath: path,
tokenFetcher: tokenFetcher,
roleARN: roleARN,
roleSessionName: roleSessionName,
}
@ -64,10 +101,16 @@ func NewWebIdentityRoleProvider(svc stsiface.STSAPI, roleARN, roleSessionName, p
// 'WebIdentityTokenFilePath' specified destination and if that is empty an
// error will be returned.
func (p *WebIdentityRoleProvider) Retrieve() (credentials.Value, error) {
b, err := ioutil.ReadFile(p.tokenFilePath)
return p.RetrieveWithContext(aws.BackgroundContext())
}
// RetrieveWithContext will attempt to assume a role from a token which is located at
// 'WebIdentityTokenFilePath' specified destination and if that is empty an
// error will be returned.
func (p *WebIdentityRoleProvider) RetrieveWithContext(ctx credentials.Context) (credentials.Value, error) {
b, err := p.tokenFetcher.FetchToken(ctx)
if err != nil {
errMsg := fmt.Sprintf("unable to read file at %s", p.tokenFilePath)
return credentials.Value{}, awserr.New(ErrCodeWebIdentity, errMsg, err)
return credentials.Value{}, awserr.New(ErrCodeWebIdentity, "failed fetching WebIdentity token: ", err)
}
sessionName := p.roleSessionName
@ -76,11 +119,22 @@ func (p *WebIdentityRoleProvider) Retrieve() (credentials.Value, error) {
// uses unix time in nanoseconds to uniquely identify sessions.
sessionName = strconv.FormatInt(now().UnixNano(), 10)
}
var duration *int64
if p.Duration != 0 {
duration = aws.Int64(int64(p.Duration / time.Second))
}
req, resp := p.client.AssumeRoleWithWebIdentityRequest(&sts.AssumeRoleWithWebIdentityInput{
PolicyArns: p.PolicyArns,
RoleArn: &p.roleARN,
RoleSessionName: &sessionName,
WebIdentityToken: aws.String(string(b)),
DurationSeconds: duration,
})
req.SetContext(ctx)
// InvalidIdentityToken error is a temporary error that can occur
// when assuming an Role with a JWT web identity token.
req.RetryErrorCodes = append(req.RetryErrorCodes, sts.ErrCodeInvalidIdentityTokenException)

View file

@ -8,6 +8,7 @@ import (
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/sdkuri"
@ -15,15 +16,16 @@ import (
// getToken uses the duration to return a token for EC2 metadata service,
// or an error if the request failed.
func (c *EC2Metadata) getToken(duration time.Duration) (tokenOutput, error) {
func (c *EC2Metadata) getToken(ctx aws.Context, duration time.Duration) (tokenOutput, error) {
op := &request.Operation{
Name: "GetToken",
HTTPMethod: "PUT",
HTTPPath: "/api/token",
HTTPPath: "/latest/api/token",
}
var output tokenOutput
req := c.NewRequest(op, nil, &output)
req.SetContext(ctx)
// remove the fetch token handler from the request handlers to avoid infinite recursion
req.Handlers.Sign.RemoveByName(fetchTokenHandlerName)
@ -50,15 +52,24 @@ func (c *EC2Metadata) getToken(duration time.Duration) (tokenOutput, error) {
// instance metadata service. The content will be returned as a string, or
// error if the request failed.
func (c *EC2Metadata) GetMetadata(p string) (string, error) {
return c.GetMetadataWithContext(aws.BackgroundContext(), p)
}
// GetMetadataWithContext uses the path provided to request information from the EC2
// instance metadata service. The content will be returned as a string, or
// error if the request failed.
func (c *EC2Metadata) GetMetadataWithContext(ctx aws.Context, p string) (string, error) {
op := &request.Operation{
Name: "GetMetadata",
HTTPMethod: "GET",
HTTPPath: sdkuri.PathJoin("/meta-data", p),
HTTPPath: sdkuri.PathJoin("/latest/meta-data", p),
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
req.SetContext(ctx)
err := req.Send()
return output.Content, err
}
@ -67,14 +78,22 @@ func (c *EC2Metadata) GetMetadata(p string) (string, error) {
// there is no user-data setup for the EC2 instance a "NotFoundError" error
// code will be returned.
func (c *EC2Metadata) GetUserData() (string, error) {
return c.GetUserDataWithContext(aws.BackgroundContext())
}
// GetUserDataWithContext returns the userdata that was configured for the service. If
// there is no user-data setup for the EC2 instance a "NotFoundError" error
// code will be returned.
func (c *EC2Metadata) GetUserDataWithContext(ctx aws.Context) (string, error) {
op := &request.Operation{
Name: "GetUserData",
HTTPMethod: "GET",
HTTPPath: "/user-data",
HTTPPath: "/latest/user-data",
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
req.SetContext(ctx)
err := req.Send()
return output.Content, err
@ -84,14 +103,22 @@ func (c *EC2Metadata) GetUserData() (string, error) {
// instance metadata service for dynamic data. The content will be returned
// as a string, or error if the request failed.
func (c *EC2Metadata) GetDynamicData(p string) (string, error) {
return c.GetDynamicDataWithContext(aws.BackgroundContext(), p)
}
// GetDynamicDataWithContext uses the path provided to request information from the EC2
// instance metadata service for dynamic data. The content will be returned
// as a string, or error if the request failed.
func (c *EC2Metadata) GetDynamicDataWithContext(ctx aws.Context, p string) (string, error) {
op := &request.Operation{
Name: "GetDynamicData",
HTTPMethod: "GET",
HTTPPath: sdkuri.PathJoin("/dynamic", p),
HTTPPath: sdkuri.PathJoin("/latest/dynamic", p),
}
output := &metadataOutput{}
req := c.NewRequest(op, nil, output)
req.SetContext(ctx)
err := req.Send()
return output.Content, err
@ -101,7 +128,14 @@ func (c *EC2Metadata) GetDynamicData(p string) (string, error) {
// instance. Error is returned if the request fails or is unable to parse
// the response.
func (c *EC2Metadata) GetInstanceIdentityDocument() (EC2InstanceIdentityDocument, error) {
resp, err := c.GetDynamicData("instance-identity/document")
return c.GetInstanceIdentityDocumentWithContext(aws.BackgroundContext())
}
// GetInstanceIdentityDocumentWithContext retrieves an identity document describing an
// instance. Error is returned if the request fails or is unable to parse
// the response.
func (c *EC2Metadata) GetInstanceIdentityDocumentWithContext(ctx aws.Context) (EC2InstanceIdentityDocument, error) {
resp, err := c.GetDynamicDataWithContext(ctx, "instance-identity/document")
if err != nil {
return EC2InstanceIdentityDocument{},
awserr.New("EC2MetadataRequestError",
@ -120,7 +154,12 @@ func (c *EC2Metadata) GetInstanceIdentityDocument() (EC2InstanceIdentityDocument
// IAMInfo retrieves IAM info from the metadata API
func (c *EC2Metadata) IAMInfo() (EC2IAMInfo, error) {
resp, err := c.GetMetadata("iam/info")
return c.IAMInfoWithContext(aws.BackgroundContext())
}
// IAMInfoWithContext retrieves IAM info from the metadata API
func (c *EC2Metadata) IAMInfoWithContext(ctx aws.Context) (EC2IAMInfo, error) {
resp, err := c.GetMetadataWithContext(ctx, "iam/info")
if err != nil {
return EC2IAMInfo{},
awserr.New("EC2MetadataRequestError",
@ -145,7 +184,12 @@ func (c *EC2Metadata) IAMInfo() (EC2IAMInfo, error) {
// Region returns the region the instance is running in.
func (c *EC2Metadata) Region() (string, error) {
ec2InstanceIdentityDocument, err := c.GetInstanceIdentityDocument()
return c.RegionWithContext(aws.BackgroundContext())
}
// RegionWithContext returns the region the instance is running in.
func (c *EC2Metadata) RegionWithContext(ctx aws.Context) (string, error) {
ec2InstanceIdentityDocument, err := c.GetInstanceIdentityDocumentWithContext(ctx)
if err != nil {
return "", err
}
@ -162,7 +206,14 @@ func (c *EC2Metadata) Region() (string, error) {
// Can be used to determine if application is running within an EC2 Instance and
// the metadata service is available.
func (c *EC2Metadata) Available() bool {
if _, err := c.GetMetadata("instance-id"); err != nil {
return c.AvailableWithContext(aws.BackgroundContext())
}
// AvailableWithContext returns if the application has access to the EC2 Metadata service.
// Can be used to determine if application is running within an EC2 Instance and
// the metadata service is available.
func (c *EC2Metadata) AvailableWithContext(ctx aws.Context) bool {
if _, err := c.GetMetadataWithContext(ctx, "instance-id"); err != nil {
return false
}

View file

@ -5,13 +5,17 @@
// variable "AWS_EC2_METADATA_DISABLED=true". This environment variable set to
// true instructs the SDK to disable the EC2 Metadata client. The client cannot
// be used while the environment variable is set to true, (case insensitive).
//
// The endpoint of the EC2 IMDS client can be configured via the environment
// variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
// Session. See aws/session#Options.EC2IMDSEndpoint for more details.
package ec2metadata
import (
"bytes"
"errors"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
@ -41,7 +45,7 @@ const (
enableTokenProviderHandlerName = "enableTokenProviderHandler"
// TTL constants
defaultTTL = 21600 * time.Second
defaultTTL = 21600 * time.Second
ttlExpirationWindow = 30 * time.Second
)
@ -69,6 +73,9 @@ func New(p client.ConfigProvider, cfgs ...*aws.Config) *EC2Metadata {
// a client when not using a session. Generally using just New with a session
// is preferred.
//
// Will remove the URL path from the endpoint provided to ensure the EC2 IMDS
// client is able to communicate with the EC2 IMDS API.
//
// If an unmodified HTTP client is provided from the stdlib default, or no client
// the EC2RoleProvider's EC2Metadata HTTP client's timeout will be shortened.
// To disable this set Config.EC2MetadataDisableTimeoutOverride to false. Enabled by default.
@ -86,6 +93,15 @@ func NewClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio
cfg.MaxRetries = aws.Int(2)
}
if u, err := url.Parse(endpoint); err == nil {
// Remove path from the endpoint since it will be added by requests.
// This is an artifact of the SDK adding `/latest` to the endpoint for
// EC2 IMDS, but this is now moved to the operation definition.
u.Path = ""
u.RawPath = ""
endpoint = u.String()
}
svc := &EC2Metadata{
Client: client.New(
cfg,
@ -217,7 +233,8 @@ func unmarshalError(r *request.Request) {
// Response body format is not consistent between metadata endpoints.
// Grab the error message as a string and include that as the source error
r.Error = awserr.NewRequestFailure(awserr.New("EC2MetadataError", "failed to make EC2Metadata request", errors.New(b.String())),
r.Error = awserr.NewRequestFailure(
awserr.New("EC2MetadataError", "failed to make EC2Metadata request\n"+b.String(), nil),
r.HTTPResponse.StatusCode, r.RequestID)
}

View file

@ -46,7 +46,7 @@ func (t *tokenProvider) fetchTokenHandler(r *request.Request) {
return
}
output, err := t.client.getToken(t.configuredTTL)
output, err := t.client.getToken(r.Context(), t.configuredTTL)
if err != nil {
@ -87,6 +87,7 @@ func (t *tokenProvider) enableTokenProviderHandler(r *request.Request) {
// If the error code status is 401, we enable the token provider
if e, ok := r.Error.(awserr.RequestFailure); ok && e != nil &&
e.StatusCode() == http.StatusUnauthorized {
t.token.Store(ec2Token{})
atomic.StoreUint32(&t.disabled, 0)
}
}

View file

@ -81,7 +81,6 @@ func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resol
// Customization
for i := 0; i < len(ps); i++ {
p := &ps[i]
custAddEC2Metadata(p)
custAddS3DualStack(p)
custRegionalS3(p)
custRmIotDataService(p)
@ -93,7 +92,7 @@ func decodeV3Endpoints(modelDef modelDefinition, opts DecodeModelOptions) (Resol
}
func custAddS3DualStack(p *partition) {
if p.ID != "aws" {
if !(p.ID == "aws" || p.ID == "aws-cn" || p.ID == "aws-us-gov") {
return
}
@ -140,19 +139,6 @@ func custAddDualstack(p *partition, svcName string) {
p.Services[svcName] = s
}
func custAddEC2Metadata(p *partition) {
p.Services["ec2metadata"] = service{
IsRegionalized: boxedFalse,
PartitionEndpoint: "aws-global",
Endpoints: endpoints{
"aws-global": endpoint{
Hostname: "169.254.169.254/latest",
Protocols: []string{"http"},
},
},
}
}
func custRmIotDataService(p *partition) {
delete(p.Services, "data.iot")
}

File diff suppressed because it is too large Load diff

View file

@ -48,6 +48,9 @@ type Options struct {
// This option is ignored if StrictMatching is enabled.
ResolveUnknownService bool
// Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6)
EC2MetadataEndpointMode EC2IMDSEndpointModeState
// STS Regional Endpoint flag helps with resolving the STS endpoint
STSRegionalEndpoint STSRegionalEndpoint
@ -55,6 +58,33 @@ type Options struct {
S3UsEast1RegionalEndpoint S3UsEast1RegionalEndpoint
}
// EC2IMDSEndpointModeState is an enum configuration variable describing the client endpoint mode.
type EC2IMDSEndpointModeState uint
// Enumeration values for EC2IMDSEndpointModeState
const (
EC2IMDSEndpointModeStateUnset EC2IMDSEndpointModeState = iota
EC2IMDSEndpointModeStateIPv4
EC2IMDSEndpointModeStateIPv6
)
// SetFromString sets the EC2IMDSEndpointModeState based on the provided string value. Unknown values will default to EC2IMDSEndpointModeStateUnset
func (e *EC2IMDSEndpointModeState) SetFromString(v string) error {
v = strings.TrimSpace(v)
switch {
case len(v) == 0:
*e = EC2IMDSEndpointModeStateUnset
case strings.EqualFold(v, "IPv6"):
*e = EC2IMDSEndpointModeStateIPv6
case strings.EqualFold(v, "IPv4"):
*e = EC2IMDSEndpointModeStateIPv4
default:
return fmt.Errorf("unknown EC2 IMDS endpoint mode, must be either IPv6 or IPv4")
}
return nil
}
// STSRegionalEndpoint is an enum for the states of the STS Regional Endpoint
// options.
type STSRegionalEndpoint int
@ -247,7 +277,7 @@ func RegionsForService(ps []Partition, partitionID, serviceID string) (map[strin
if p.ID() != partitionID {
continue
}
if _, ok := p.p.Services[serviceID]; !ok {
if _, ok := p.p.Services[serviceID]; !(ok || serviceID == Ec2metadataServiceID) {
break
}
@ -333,6 +363,7 @@ func (p Partition) Regions() map[string]Region {
// enumerating over the services in a partition.
func (p Partition) Services() map[string]Service {
ss := make(map[string]Service, len(p.p.Services))
for id := range p.p.Services {
ss[id] = Service{
id: id,
@ -340,6 +371,15 @@ func (p Partition) Services() map[string]Service {
}
}
// Since we have removed the customization that injected this into the model
// we still need to pretend that this is a modeled service.
if _, ok := ss[Ec2metadataServiceID]; !ok {
ss[Ec2metadataServiceID] = Service{
id: Ec2metadataServiceID,
p: p.p,
}
}
return ss
}
@ -400,7 +440,18 @@ func (s Service) ResolveEndpoint(region string, opts ...func(*Options)) (Resolve
// an URL that can be resolved to a instance of a service.
func (s Service) Regions() map[string]Region {
rs := map[string]Region{}
for id := range s.p.Services[s.id].Endpoints {
service, ok := s.p.Services[s.id]
// Since ec2metadata customization has been removed we need to check
// if it was defined in non-standard endpoints.json file. If it's not
// then we can return the empty map as there is no regional-endpoints for IMDS.
// Otherwise, we iterate need to iterate the non-standard model.
if s.id == Ec2metadataServiceID && !ok {
return rs
}
for id := range service.Endpoints {
if r, ok := s.p.Regions[id]; ok {
rs[id] = Region{
id: id,

View file

@ -7,6 +7,13 @@ import (
"strings"
)
const (
ec2MetadataEndpointIPv6 = "http://[fd00:ec2::254]/latest"
ec2MetadataEndpointIPv4 = "http://169.254.169.254/latest"
)
var regionValidationRegex = regexp.MustCompile(`^[[:alnum:]]([[:alnum:]\-]*[[:alnum:]])?$`)
type partitions []partition
func (ps partitions) EndpointFor(service, region string, opts ...func(*Options)) (ResolvedEndpoint, error) {
@ -100,6 +107,12 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (
opt.Set(opts...)
s, hasService := p.Services[service]
if service == Ec2metadataServiceID && !hasService {
endpoint := getEC2MetadataEndpoint(p.ID, service, opt.EC2MetadataEndpointMode)
return endpoint, nil
}
if len(service) == 0 || !(hasService || opt.ResolveUnknownService) {
// Only return error if the resolver will not fallback to creating
// endpoint based on service endpoint ID passed in.
@ -124,7 +137,32 @@ func (p partition) EndpointFor(service, region string, opts ...func(*Options)) (
defs := []endpoint{p.Defaults, s.Defaults}
return e.resolve(service, p.ID, region, p.DNSSuffix, defs, opt), nil
return e.resolve(service, p.ID, region, p.DNSSuffix, defs, opt)
}
func getEC2MetadataEndpoint(partitionID, service string, mode EC2IMDSEndpointModeState) ResolvedEndpoint {
switch mode {
case EC2IMDSEndpointModeStateIPv6:
return ResolvedEndpoint{
URL: ec2MetadataEndpointIPv6,
PartitionID: partitionID,
SigningRegion: "aws-global",
SigningName: service,
SigningNameDerived: true,
SigningMethod: "v4",
}
case EC2IMDSEndpointModeStateIPv4:
fallthrough
default:
return ResolvedEndpoint{
URL: ec2MetadataEndpointIPv4,
PartitionID: partitionID,
SigningRegion: "aws-global",
SigningName: service,
SigningNameDerived: true,
SigningMethod: "v4",
}
}
}
func serviceList(ss services) []string {
@ -176,14 +214,14 @@ type service struct {
}
func (s *service) endpointForRegion(region string) (endpoint, bool) {
if s.IsRegionalized == boxedFalse {
return s.Endpoints[s.PartitionEndpoint], region == s.PartitionEndpoint
}
if e, ok := s.Endpoints[region]; ok {
return e, true
}
if s.IsRegionalized == boxedFalse {
return s.Endpoints[s.PartitionEndpoint], region == s.PartitionEndpoint
}
// Unable to find any matching endpoint, return
// blank that will be used for generic endpoint creation.
return endpoint{}, false
@ -233,7 +271,7 @@ func getByPriority(s []string, p []string, def string) string {
return s[0]
}
func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs []endpoint, opts Options) ResolvedEndpoint {
func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs []endpoint, opts Options) (ResolvedEndpoint, error) {
var merged endpoint
for _, def := range defs {
merged.mergeIn(def)
@ -260,6 +298,10 @@ func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs [
region = signingRegion
}
if !validateInputRegion(region) {
return ResolvedEndpoint{}, fmt.Errorf("invalid region identifier format provided")
}
u := strings.Replace(hostname, "{service}", service, 1)
u = strings.Replace(u, "{region}", region, 1)
u = strings.Replace(u, "{dnsSuffix}", dnsSuffix, 1)
@ -274,7 +316,7 @@ func (e endpoint) resolve(service, partitionID, region, dnsSuffix string, defs [
SigningName: signingName,
SigningNameDerived: signingNameDerived,
SigningMethod: getByPriority(e.SignatureVersions, signerPriority, defaultSigner),
}
}, nil
}
func getEndpointScheme(protocols []string, disableSSL bool) string {
@ -339,3 +381,7 @@ const (
boxedFalse
boxedTrue
)
func validateInputRegion(region string) bool {
return regionValidationRegex.MatchString(region)
}

View file

@ -1,3 +1,4 @@
//go:build codegen
// +build codegen
package endpoints

View file

@ -9,7 +9,8 @@ func isErrConnectionReset(err error) bool {
return false
}
if strings.Contains(err.Error(), "connection reset") ||
if strings.Contains(err.Error(), "use of closed network connection") ||
strings.Contains(err.Error(), "connection reset") ||
strings.Contains(err.Error(), "broken pipe") {
return true
}

View file

@ -129,13 +129,26 @@ func New(cfg aws.Config, clientInfo metadata.ClientInfo, handlers Handlers,
httpReq, _ := http.NewRequest(method, "", nil)
var err error
httpReq.URL, err = url.Parse(clientInfo.Endpoint + operation.HTTPPath)
httpReq.URL, err = url.Parse(clientInfo.Endpoint)
if err != nil {
httpReq.URL = &url.URL{}
err = awserr.New("InvalidEndpointURL", "invalid endpoint uri", err)
}
SanitizeHostForHeader(httpReq)
if len(operation.HTTPPath) != 0 {
opHTTPPath := operation.HTTPPath
var opQueryString string
if idx := strings.Index(opHTTPPath, "?"); idx >= 0 {
opQueryString = opHTTPPath[idx+1:]
opHTTPPath = opHTTPPath[:idx]
}
if strings.HasSuffix(httpReq.URL.Path, "/") && strings.HasPrefix(opHTTPPath, "/") {
opHTTPPath = opHTTPPath[1:]
}
httpReq.URL.Path += opHTTPPath
httpReq.URL.RawQuery = opQueryString
}
r := &Request{
Config: cfg,
@ -426,6 +439,8 @@ func (r *Request) Sign() error {
return r.Error
}
SanitizeHostForHeader(r.HTTPRequest)
r.Handlers.Sign.Run(r)
return r.Error
}
@ -639,6 +654,10 @@ func getHost(r *http.Request) string {
return r.Host
}
if r.URL == nil {
return ""
}
return r.URL.Host
}

View file

@ -1,3 +1,4 @@
//go:build !go1.8
// +build !go1.8
package request

View file

@ -1,3 +1,4 @@
//go:build go1.8
// +build go1.8
package request

View file

@ -1,3 +1,4 @@
//go:build go1.7
// +build go1.7
package request

View file

@ -1,3 +1,4 @@
//go:build !go1.7
// +build !go1.7
package request

View file

@ -92,6 +92,7 @@ var throttleCodes = map[string]struct{}{
"TooManyRequestsException": {}, // Lambda functions
"PriorRequestNotComplete": {}, // Route53
"TransactionInProgressException": {},
"EC2ThrottledException": {}, // EC2
}
// credsExpiredCodes is a collection of error codes which signify the credentials

View file

@ -3,11 +3,13 @@ package session
import (
"fmt"
"os"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/processcreds"
"github.com/aws/aws-sdk-go/aws/credentials/ssocreds"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/defaults"
"github.com/aws/aws-sdk-go/aws/request"
@ -99,10 +101,6 @@ func resolveCredsFromProfile(cfg *aws.Config,
sharedCfg.Creds,
)
case len(sharedCfg.CredentialProcess) != 0:
// Get credentials from CredentialProcess
creds = processcreds.NewCredentials(sharedCfg.CredentialProcess)
case len(sharedCfg.CredentialSource) != 0:
creds, err = resolveCredsFromSource(cfg, envCfg,
sharedCfg, handlers, sessOpts,
@ -118,6 +116,13 @@ func resolveCredsFromProfile(cfg *aws.Config,
sharedCfg.RoleSessionName,
)
case sharedCfg.hasSSOConfiguration():
creds, err = resolveSSOCredentials(cfg, sharedCfg, handlers)
case len(sharedCfg.CredentialProcess) != 0:
// Get credentials from CredentialProcess
creds = processcreds.NewCredentials(sharedCfg.CredentialProcess)
default:
// Fallback to default credentials provider, include mock errors for
// the credential chain so user can identify why credentials failed to
@ -150,6 +155,25 @@ func resolveCredsFromProfile(cfg *aws.Config,
return creds, nil
}
func resolveSSOCredentials(cfg *aws.Config, sharedCfg sharedConfig, handlers request.Handlers) (*credentials.Credentials, error) {
if err := sharedCfg.validateSSOConfiguration(); err != nil {
return nil, err
}
cfgCopy := cfg.Copy()
cfgCopy.Region = &sharedCfg.SSORegion
return ssocreds.NewCredentials(
&Session{
Config: cfgCopy,
Handlers: handlers.Copy(),
},
sharedCfg.SSOAccountID,
sharedCfg.SSORoleName,
sharedCfg.SSOStartURL,
), nil
}
// valid credential source values
const (
credSourceEc2Metadata = "Ec2InstanceMetadata"
@ -206,7 +230,14 @@ func credsFromAssumeRole(cfg aws.Config,
sharedCfg.RoleARN,
func(opt *stscreds.AssumeRoleProvider) {
opt.RoleSessionName = sharedCfg.RoleSessionName
opt.Duration = sessOpts.AssumeRoleDuration
if sessOpts.AssumeRoleDuration == 0 &&
sharedCfg.AssumeRoleDuration != nil &&
*sharedCfg.AssumeRoleDuration/time.Minute > 15 {
opt.Duration = *sharedCfg.AssumeRoleDuration
} else if sessOpts.AssumeRoleDuration != 0 {
opt.Duration = sessOpts.AssumeRoleDuration
}
// Assume role with external ID
if len(sharedCfg.ExternalID) > 0 {

View file

@ -0,0 +1,28 @@
//go:build go1.13
// +build go1.13
package session
import (
"net"
"net/http"
"time"
)
// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
}

View file

@ -1,4 +1,5 @@
// +build go1.7
//go:build !go1.13 && go1.7
// +build !go1.13,go1.7
package session
@ -10,7 +11,7 @@ import (
// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{

View file

@ -1,3 +1,4 @@
//go:build !go1.6 && go1.5
// +build !go1.6,go1.5
package session
@ -10,7 +11,7 @@ import (
// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{

View file

@ -1,3 +1,4 @@
//go:build !go1.7 && go1.6
// +build !go1.7,go1.6
package session
@ -10,7 +11,7 @@ import (
// Transport that should be used when a custom CA bundle is specified with the
// SDK.
func getCABundleTransport() *http.Transport {
func getCustomTransport() *http.Transport {
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{

View file

@ -208,6 +208,8 @@ env values as well.
AWS_SDK_LOAD_CONFIG=1
Custom Shared Config and Credential Files
Shared credentials file path can be set to instruct the SDK to use an alternative
file for the shared credentials. If not set the file will be loaded from
$HOME/.aws/credentials on Linux/Unix based systems, and
@ -222,6 +224,8 @@ $HOME/.aws/config on Linux/Unix based systems, and
AWS_CONFIG_FILE=$HOME/my_shared_config
Custom CA Bundle
Path to a custom Credentials Authority (CA) bundle PEM file that the SDK
will use instead of the default system's root CA bundle. Use this only
if you want to replace the CA bundle the SDK uses for TLS requests.
@ -241,5 +245,45 @@ over the AWS_CA_BUNDLE environment variable, and will be used if both are set.
Setting a custom HTTPClient in the aws.Config options will override this setting.
To use this option and custom HTTP client, the HTTP client needs to be provided
when creating the session. Not the service client.
Custom Client TLS Certificate
The SDK supports the environment and session option being configured with
Client TLS certificates that are sent as a part of the client's TLS handshake
for client authentication. If used, both Cert and Key values are required. If
one is missing, or either fail to load the contents of the file an error will
be returned.
HTTP Client's Transport concrete implementation must be a http.Transport
or creating the session will fail.
AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
This can also be configured via the session.Options ClientTLSCert and ClientTLSKey.
sess, err := session.NewSessionWithOptions(session.Options{
ClientTLSCert: myCertFile,
ClientTLSKey: myKeyFile,
})
Custom EC2 IMDS Endpoint
The endpoint of the EC2 IMDS client can be configured via the environment
variable, AWS_EC2_METADATA_SERVICE_ENDPOINT when creating the client with a
Session. See Options.EC2IMDSEndpoint for more details.
AWS_EC2_METADATA_SERVICE_ENDPOINT=http://169.254.169.254
If using an URL with an IPv6 address literal, the IPv6 address
component must be enclosed in square brackets.
AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1]
The custom EC2 IMDS endpoint can also be specified via the Session options.
sess, err := session.NewSessionWithOptions(session.Options{
EC2MetadataEndpoint: "http://[::1]",
})
*/
package session

View file

@ -101,6 +101,18 @@ type envConfig struct {
// AWS_CA_BUNDLE=$HOME/my_custom_ca_bundle
CustomCABundle string
// Sets the TLC client certificate that should be used by the SDK's HTTP transport
// when making requests. The certificate must be paired with a TLS client key file.
//
// AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
ClientTLSCert string
// Sets the TLC client key that should be used by the SDK's HTTP transport
// when making requests. The key must be paired with a TLS client certificate file.
//
// AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
ClientTLSKey string
csmEnabled string
CSMEnabled *bool
CSMPort string
@ -148,6 +160,16 @@ type envConfig struct {
//
// AWS_S3_USE_ARN_REGION=true
S3UseARNRegion bool
// Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EC2IMDSEndpointMode.
//
// AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1]
EC2IMDSEndpoint string
// Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6)
//
// AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE=IPv6
EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState
}
var (
@ -211,6 +233,21 @@ var (
s3UseARNRegionEnvKey = []string{
"AWS_S3_USE_ARN_REGION",
}
ec2IMDSEndpointEnvKey = []string{
"AWS_EC2_METADATA_SERVICE_ENDPOINT",
}
ec2IMDSEndpointModeEnvKey = []string{
"AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE",
}
useCABundleKey = []string{
"AWS_CA_BUNDLE",
}
useClientTLSCert = []string{
"AWS_SDK_GO_CLIENT_TLS_CERT",
}
useClientTLSKey = []string{
"AWS_SDK_GO_CLIENT_TLS_KEY",
}
)
// loadEnvConfig retrieves the SDK's environment configuration.
@ -294,7 +331,9 @@ func envConfigLoad(enableSharedConfig bool) (envConfig, error) {
cfg.SharedConfigFile = defaults.SharedConfigFilename()
}
cfg.CustomCABundle = os.Getenv("AWS_CA_BUNDLE")
setFromEnvVal(&cfg.CustomCABundle, useCABundleKey)
setFromEnvVal(&cfg.ClientTLSCert, useClientTLSCert)
setFromEnvVal(&cfg.ClientTLSKey, useClientTLSKey)
var err error
// STS Regional Endpoint variable
@ -332,6 +371,11 @@ func envConfigLoad(enableSharedConfig bool) (envConfig, error) {
}
}
setFromEnvVal(&cfg.EC2IMDSEndpoint, ec2IMDSEndpointEnvKey)
if err := setEC2IMDSEndpointMode(&cfg.EC2IMDSEndpointMode, ec2IMDSEndpointModeEnvKey); err != nil {
return envConfig{}, err
}
return cfg, nil
}
@ -343,3 +387,17 @@ func setFromEnvVal(dst *string, keys []string) {
}
}
}
func setEC2IMDSEndpointMode(mode *endpoints.EC2IMDSEndpointModeState, keys []string) error {
for _, k := range keys {
value := os.Getenv(k)
if len(value) == 0 {
continue
}
if err := mode.SetFromString(value); err != nil {
return fmt.Errorf("invalid value for environment variable, %s=%s, %v", k, value, err)
}
return nil
}
return nil
}

View file

@ -25,11 +25,18 @@ const (
// ErrCodeSharedConfig represents an error that occurs in the shared
// configuration logic
ErrCodeSharedConfig = "SharedConfigErr"
// ErrCodeLoadCustomCABundle error code for unable to load custom CA bundle.
ErrCodeLoadCustomCABundle = "LoadCustomCABundleError"
// ErrCodeLoadClientTLSCert error code for unable to load client TLS
// certificate or key
ErrCodeLoadClientTLSCert = "LoadClientTLSCertError"
)
// ErrSharedConfigSourceCollision will be returned if a section contains both
// source_profile and credential_source
var ErrSharedConfigSourceCollision = awserr.New(ErrCodeSharedConfig, "only source profile or credential source can be specified, not both", nil)
var ErrSharedConfigSourceCollision = awserr.New(ErrCodeSharedConfig, "only one credential type may be specified per profile: source profile, credential source, credential process, web identity token, or sso", nil)
// ErrSharedConfigECSContainerEnvVarEmpty will be returned if the environment
// variables are empty and Environment was set as the credential source
@ -48,6 +55,8 @@ var ErrSharedConfigInvalidCredSource = awserr.New(ErrCodeSharedConfig, "credenti
type Session struct {
Config *aws.Config
Handlers request.Handlers
options Options
}
// New creates a new instance of the handlers merging in the provided configs
@ -99,7 +108,7 @@ func New(cfgs ...*aws.Config) *Session {
return s
}
s := deprecatedNewSession(cfgs...)
s := deprecatedNewSession(envCfg, cfgs...)
if envErr != nil {
msg := "failed to load env config"
s.logDeprecatedNewSessionError(msg, envErr, cfgs)
@ -227,22 +236,73 @@ type Options struct {
// the SDK will use instead of the default system's root CA bundle. Use this
// only if you want to replace the CA bundle the SDK uses for TLS requests.
//
// Enabling this option will attempt to merge the Transport into the SDK's HTTP
// client. If the client's Transport is not a http.Transport an error will be
// returned. If the Transport's TLS config is set this option will cause the SDK
// HTTP Client's Transport concrete implementation must be a http.Transport
// or creating the session will fail.
//
// If the Transport's TLS config is set this option will cause the SDK
// to overwrite the Transport's TLS config's RootCAs value. If the CA
// bundle reader contains multiple certificates all of them will be loaded.
//
// The Session option CustomCABundle is also available when creating sessions
// to also enable this feature. CustomCABundle session option field has priority
// over the AWS_CA_BUNDLE environment variable, and will be used if both are set.
// Can also be specified via the environment variable:
//
// AWS_CA_BUNDLE=$HOME/ca_bundle
//
// Can also be specified via the shared config field:
//
// ca_bundle = $HOME/ca_bundle
CustomCABundle io.Reader
// Reader for the TLC client certificate that should be used by the SDK's
// HTTP transport when making requests. The certificate must be paired with
// a TLS client key file. Will be ignored if both are not provided.
//
// HTTP Client's Transport concrete implementation must be a http.Transport
// or creating the session will fail.
//
// Can also be specified via the environment variable:
//
// AWS_SDK_GO_CLIENT_TLS_CERT=$HOME/my_client_cert
ClientTLSCert io.Reader
// Reader for the TLC client key that should be used by the SDK's HTTP
// transport when making requests. The key must be paired with a TLS client
// certificate file. Will be ignored if both are not provided.
//
// HTTP Client's Transport concrete implementation must be a http.Transport
// or creating the session will fail.
//
// Can also be specified via the environment variable:
//
// AWS_SDK_GO_CLIENT_TLS_KEY=$HOME/my_client_key
ClientTLSKey io.Reader
// The handlers that the session and all API clients will be created with.
// This must be a complete set of handlers. Use the defaults.Handlers()
// function to initialize this value before changing the handlers to be
// used by the SDK.
Handlers request.Handlers
// Allows specifying a custom endpoint to be used by the EC2 IMDS client
// when making requests to the EC2 IMDS API. The endpoint value should
// include the URI scheme. If the scheme is not present it will be defaulted to http.
//
// If unset, will the EC2 IMDS client will use its default endpoint.
//
// Can also be specified via the environment variable,
// AWS_EC2_METADATA_SERVICE_ENDPOINT.
//
// AWS_EC2_METADATA_SERVICE_ENDPOINT=http://169.254.169.254
//
// If using an URL with an IPv6 address literal, the IPv6 address
// component must be enclosed in square brackets.
//
// AWS_EC2_METADATA_SERVICE_ENDPOINT=http://[::1]
EC2IMDSEndpoint string
// Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6)
//
// AWS_EC2_METADATA_SERVICE_ENDPOINT_MODE=IPv6
EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState
}
// NewSessionWithOptions returns a new Session created from SDK defaults, config files,
@ -300,17 +360,6 @@ func NewSessionWithOptions(opts Options) (*Session, error) {
envCfg.EnableSharedConfig = true
}
// Only use AWS_CA_BUNDLE if session option is not provided.
if len(envCfg.CustomCABundle) != 0 && opts.CustomCABundle == nil {
f, err := os.Open(envCfg.CustomCABundle)
if err != nil {
return nil, awserr.New("LoadCustomCABundleError",
"failed to open custom CA bundle PEM file", err)
}
defer f.Close()
opts.CustomCABundle = f
}
return newSession(opts, envCfg, &opts.Config)
}
@ -329,7 +378,29 @@ func Must(sess *Session, err error) *Session {
return sess
}
func deprecatedNewSession(cfgs ...*aws.Config) *Session {
// Wraps the endpoint resolver with a resolver that will return a custom
// endpoint for EC2 IMDS.
func wrapEC2IMDSEndpoint(resolver endpoints.Resolver, endpoint string, mode endpoints.EC2IMDSEndpointModeState) endpoints.Resolver {
return endpoints.ResolverFunc(
func(service, region string, opts ...func(*endpoints.Options)) (
endpoints.ResolvedEndpoint, error,
) {
if service == ec2MetadataServiceID && len(endpoint) > 0 {
return endpoints.ResolvedEndpoint{
URL: endpoint,
SigningName: ec2MetadataServiceID,
SigningRegion: region,
}, nil
} else if service == ec2MetadataServiceID {
opts = append(opts, func(o *endpoints.Options) {
o.EC2MetadataEndpointMode = mode
})
}
return resolver.EndpointFor(service, region, opts...)
})
}
func deprecatedNewSession(envCfg envConfig, cfgs ...*aws.Config) *Session {
cfg := defaults.Config()
handlers := defaults.Handlers()
@ -341,6 +412,11 @@ func deprecatedNewSession(cfgs ...*aws.Config) *Session {
// endpoints for service client configurations.
cfg.EndpointResolver = endpoints.DefaultResolver()
}
if !(len(envCfg.EC2IMDSEndpoint) == 0 && envCfg.EC2IMDSEndpointMode == endpoints.EC2IMDSEndpointModeStateUnset) {
cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, envCfg.EC2IMDSEndpoint, envCfg.EC2IMDSEndpointMode)
}
cfg.Credentials = defaults.CredChain(cfg, handlers)
// Reapply any passed in configs to override credentials if set
@ -349,6 +425,9 @@ func deprecatedNewSession(cfgs ...*aws.Config) *Session {
s := &Session{
Config: cfg,
Handlers: handlers,
options: Options{
EC2IMDSEndpoint: envCfg.EC2IMDSEndpoint,
},
}
initHandlers(s)
@ -415,9 +494,14 @@ func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session,
return nil, err
}
if err := setTLSOptions(&opts, cfg, envCfg, sharedCfg); err != nil {
return nil, err
}
s := &Session{
Config: cfg,
Handlers: handlers,
options: opts,
}
initHandlers(s)
@ -433,13 +517,6 @@ func newSession(opts Options, envCfg envConfig, cfgs ...*aws.Config) (*Session,
}
}
// Setup HTTP client with custom cert bundle if enabled
if opts.CustomCABundle != nil {
if err := loadCustomCABundle(s, opts.CustomCABundle); err != nil {
return nil, err
}
}
return s, nil
}
@ -483,22 +560,83 @@ func loadCSMConfig(envCfg envConfig, cfgFiles []string) (csmConfig, error) {
return csmConfig{}, nil
}
func loadCustomCABundle(s *Session, bundle io.Reader) error {
func setTLSOptions(opts *Options, cfg *aws.Config, envCfg envConfig, sharedCfg sharedConfig) error {
// CA Bundle can be specified in both environment variable shared config file.
var caBundleFilename = envCfg.CustomCABundle
if len(caBundleFilename) == 0 {
caBundleFilename = sharedCfg.CustomCABundle
}
// Only use environment value if session option is not provided.
customTLSOptions := map[string]struct {
filename string
field *io.Reader
errCode string
}{
"custom CA bundle PEM": {filename: caBundleFilename, field: &opts.CustomCABundle, errCode: ErrCodeLoadCustomCABundle},
"custom client TLS cert": {filename: envCfg.ClientTLSCert, field: &opts.ClientTLSCert, errCode: ErrCodeLoadClientTLSCert},
"custom client TLS key": {filename: envCfg.ClientTLSKey, field: &opts.ClientTLSKey, errCode: ErrCodeLoadClientTLSCert},
}
for name, v := range customTLSOptions {
if len(v.filename) != 0 && *v.field == nil {
f, err := os.Open(v.filename)
if err != nil {
return awserr.New(v.errCode, fmt.Sprintf("failed to open %s file", name), err)
}
defer f.Close()
*v.field = f
}
}
// Setup HTTP client with custom cert bundle if enabled
if opts.CustomCABundle != nil {
if err := loadCustomCABundle(cfg.HTTPClient, opts.CustomCABundle); err != nil {
return err
}
}
// Setup HTTP client TLS certificate and key for client TLS authentication.
if opts.ClientTLSCert != nil && opts.ClientTLSKey != nil {
if err := loadClientTLSCert(cfg.HTTPClient, opts.ClientTLSCert, opts.ClientTLSKey); err != nil {
return err
}
} else if opts.ClientTLSCert == nil && opts.ClientTLSKey == nil {
// Do nothing if neither values are available.
} else {
return awserr.New(ErrCodeLoadClientTLSCert,
fmt.Sprintf("client TLS cert(%t) and key(%t) must both be provided",
opts.ClientTLSCert != nil, opts.ClientTLSKey != nil), nil)
}
return nil
}
func getHTTPTransport(client *http.Client) (*http.Transport, error) {
var t *http.Transport
switch v := s.Config.HTTPClient.Transport.(type) {
switch v := client.Transport.(type) {
case *http.Transport:
t = v
default:
if s.Config.HTTPClient.Transport != nil {
return awserr.New("LoadCustomCABundleError",
"unable to load custom CA bundle, HTTPClient's transport unsupported type", nil)
if client.Transport != nil {
return nil, fmt.Errorf("unsupported transport, %T", client.Transport)
}
}
if t == nil {
// Nil transport implies `http.DefaultTransport` should be used. Since
// the SDK cannot modify, nor copy the `DefaultTransport` specifying
// the values the next closest behavior.
t = getCABundleTransport()
t = getCustomTransport()
}
return t, nil
}
func loadCustomCABundle(client *http.Client, bundle io.Reader) error {
t, err := getHTTPTransport(client)
if err != nil {
return awserr.New(ErrCodeLoadCustomCABundle,
"unable to load custom CA bundle, HTTPClient's transport unsupported type", err)
}
p, err := loadCertPool(bundle)
@ -510,7 +648,7 @@ func loadCustomCABundle(s *Session, bundle io.Reader) error {
}
t.TLSClientConfig.RootCAs = p
s.Config.HTTPClient.Transport = t
client.Transport = t
return nil
}
@ -518,19 +656,57 @@ func loadCustomCABundle(s *Session, bundle io.Reader) error {
func loadCertPool(r io.Reader) (*x509.CertPool, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, awserr.New("LoadCustomCABundleError",
return nil, awserr.New(ErrCodeLoadCustomCABundle,
"failed to read custom CA bundle PEM file", err)
}
p := x509.NewCertPool()
if !p.AppendCertsFromPEM(b) {
return nil, awserr.New("LoadCustomCABundleError",
return nil, awserr.New(ErrCodeLoadCustomCABundle,
"failed to load custom CA bundle PEM file", err)
}
return p, nil
}
func loadClientTLSCert(client *http.Client, certFile, keyFile io.Reader) error {
t, err := getHTTPTransport(client)
if err != nil {
return awserr.New(ErrCodeLoadClientTLSCert,
"unable to get usable HTTP transport from client", err)
}
cert, err := ioutil.ReadAll(certFile)
if err != nil {
return awserr.New(ErrCodeLoadClientTLSCert,
"unable to get read client TLS cert file", err)
}
key, err := ioutil.ReadAll(keyFile)
if err != nil {
return awserr.New(ErrCodeLoadClientTLSCert,
"unable to get read client TLS key file", err)
}
clientCert, err := tls.X509KeyPair(cert, key)
if err != nil {
return awserr.New(ErrCodeLoadClientTLSCert,
"unable to load x509 key pair from client cert", err)
}
tlsCfg := t.TLSClientConfig
if tlsCfg == nil {
tlsCfg = &tls.Config{}
}
tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
t.TLSClientConfig = tlsCfg
client.Transport = t
return nil
}
func mergeConfigSrcs(cfg, userCfg *aws.Config,
envCfg envConfig, sharedCfg sharedConfig,
handlers request.Handlers,
@ -570,6 +746,34 @@ func mergeConfigSrcs(cfg, userCfg *aws.Config,
endpoints.LegacyS3UsEast1Endpoint,
})
var ec2IMDSEndpoint string
for _, v := range []string{
sessOpts.EC2IMDSEndpoint,
envCfg.EC2IMDSEndpoint,
sharedCfg.EC2IMDSEndpoint,
} {
if len(v) != 0 {
ec2IMDSEndpoint = v
break
}
}
var endpointMode endpoints.EC2IMDSEndpointModeState
for _, v := range []endpoints.EC2IMDSEndpointModeState{
sessOpts.EC2IMDSEndpointMode,
envCfg.EC2IMDSEndpointMode,
sharedCfg.EC2IMDSEndpointMode,
} {
if v != endpoints.EC2IMDSEndpointModeStateUnset {
endpointMode = v
break
}
}
if len(ec2IMDSEndpoint) != 0 || endpointMode != endpoints.EC2IMDSEndpointModeStateUnset {
cfg.EndpointResolver = wrapEC2IMDSEndpoint(cfg.EndpointResolver, ec2IMDSEndpoint, endpointMode)
}
// Configure credentials if not already set by the user when creating the
// Session.
if cfg.Credentials == credentials.AnonymousCredentials && userCfg.Credentials == nil {
@ -627,6 +831,7 @@ func (s *Session) Copy(cfgs ...*aws.Config) *Session {
newSession := &Session{
Config: s.Config.Copy(cfgs...),
Handlers: s.Handlers.Copy(),
options: s.options,
}
initHandlers(newSession)
@ -665,6 +870,8 @@ func (s *Session) ClientConfig(service string, cfgs ...*aws.Config) client.Confi
}
}
const ec2MetadataServiceID = "ec2metadata"
func (s *Session) resolveEndpoint(service, region string, cfg *aws.Config) (endpoints.ResolvedEndpoint, error) {
if ep := aws.StringValue(cfg.Endpoint); len(ep) != 0 {

View file

@ -2,6 +2,8 @@ package session
import (
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
@ -16,12 +18,19 @@ const (
sessionTokenKey = `aws_session_token` // optional
// Assume Role Credentials group
roleArnKey = `role_arn` // group required
sourceProfileKey = `source_profile` // group required (or credential_source)
credentialSourceKey = `credential_source` // group required (or source_profile)
externalIDKey = `external_id` // optional
mfaSerialKey = `mfa_serial` // optional
roleSessionNameKey = `role_session_name` // optional
roleArnKey = `role_arn` // group required
sourceProfileKey = `source_profile` // group required (or credential_source)
credentialSourceKey = `credential_source` // group required (or source_profile)
externalIDKey = `external_id` // optional
mfaSerialKey = `mfa_serial` // optional
roleSessionNameKey = `role_session_name` // optional
roleDurationSecondsKey = "duration_seconds" // optional
// AWS Single Sign-On (AWS SSO) group
ssoAccountIDKey = "sso_account_id"
ssoRegionKey = "sso_region"
ssoRoleNameKey = "sso_role_name"
ssoStartURL = "sso_start_url"
// CSM options
csmEnabledKey = `csm_enabled`
@ -32,6 +41,9 @@ const (
// Additional Config fields
regionKey = `region`
// custom CA Bundle filename
customCABundleKey = `ca_bundle`
// endpoint discovery group
enableEndpointDiscoveryKey = `endpoint_discovery_enabled` // optional
@ -54,10 +66,18 @@ const (
// S3 ARN Region Usage
s3UseARNRegionKey = "s3_use_arn_region"
// EC2 IMDS Endpoint Mode
ec2MetadataServiceEndpointModeKey = "ec2_metadata_service_endpoint_mode"
// EC2 IMDS Endpoint
ec2MetadataServiceEndpointKey = "ec2_metadata_service_endpoint"
)
// sharedConfig represents the configuration fields of the SDK config files.
type sharedConfig struct {
Profile string
// Credentials values from the config file. Both aws_access_key_id and
// aws_secret_access_key must be provided together in the same file to be
// considered valid. The values will be ignored if not a complete group.
@ -73,10 +93,16 @@ type sharedConfig struct {
CredentialProcess string
WebIdentityTokenFile string
RoleARN string
RoleSessionName string
ExternalID string
MFASerial string
SSOAccountID string
SSORegion string
SSORoleName string
SSOStartURL string
RoleARN string
RoleSessionName string
ExternalID string
MFASerial string
AssumeRoleDuration *time.Duration
SourceProfileName string
SourceProfile *sharedConfig
@ -87,6 +113,15 @@ type sharedConfig struct {
// region
Region string
// CustomCABundle is the file path to a PEM file the SDK will read and
// use to configure the HTTP transport with additional CA certs that are
// not present in the platforms default CA store.
//
// This value will be ignored if the file does not exist.
//
// ca_bundle
CustomCABundle string
// EnableEndpointDiscovery can be enabled in the shared config by setting
// endpoint_discovery_enabled to true
//
@ -116,6 +151,16 @@ type sharedConfig struct {
//
// s3_use_arn_region=true
S3UseARNRegion bool
// Specifies the EC2 Instance Metadata Service default endpoint selection mode (IPv4 or IPv6)
//
// ec2_metadata_service_endpoint_mode=IPv6
EC2IMDSEndpointMode endpoints.EC2IMDSEndpointModeState
// Specifies the EC2 Instance Metadata Service endpoint to use. If specified it overrides EC2IMDSEndpointMode.
//
// ec2_metadata_service_endpoint=http://fd00:ec2::254
EC2IMDSEndpoint string
}
type sharedConfigFile struct {
@ -174,6 +219,8 @@ func loadSharedConfigIniFiles(filenames []string) ([]sharedConfigFile, error) {
}
func (cfg *sharedConfig) setFromIniFiles(profiles map[string]struct{}, profile string, files []sharedConfigFile, exOpts bool) error {
cfg.Profile = profile
// Trim files from the list that don't exist.
var skippedFiles int
var profileNotFoundErr error
@ -202,9 +249,9 @@ func (cfg *sharedConfig) setFromIniFiles(profiles map[string]struct{}, profile s
cfg.clearAssumeRoleOptions()
} else {
// First time a profile has been seen, It must either be a assume role
// or credentials. Assert if the credential type requires a role ARN,
// the ARN is also set.
if err := cfg.validateCredentialsRequireARN(profile); err != nil {
// credentials, or SSO. Assert if the credential type requires a role ARN,
// the ARN is also set, or validate that the SSO configuration is complete.
if err := cfg.validateCredentialsConfig(profile); err != nil {
return err
}
}
@ -273,6 +320,12 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e
updateString(&cfg.SourceProfileName, section, sourceProfileKey)
updateString(&cfg.CredentialSource, section, credentialSourceKey)
updateString(&cfg.Region, section, regionKey)
updateString(&cfg.CustomCABundle, section, customCABundleKey)
if section.Has(roleDurationSecondsKey) {
d := time.Duration(section.Int(roleDurationSecondsKey)) * time.Second
cfg.AssumeRoleDuration = &d
}
if v := section.String(stsRegionalEndpointSharedKey); len(v) != 0 {
sre, err := endpoints.GetSTSRegionalEndpoint(v)
@ -291,6 +344,18 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e
}
cfg.S3UsEast1RegionalEndpoint = sre
}
// AWS Single Sign-On (AWS SSO)
updateString(&cfg.SSOAccountID, section, ssoAccountIDKey)
updateString(&cfg.SSORegion, section, ssoRegionKey)
updateString(&cfg.SSORoleName, section, ssoRoleNameKey)
updateString(&cfg.SSOStartURL, section, ssoStartURL)
if err := updateEC2MetadataServiceEndpointMode(&cfg.EC2IMDSEndpointMode, section, ec2MetadataServiceEndpointModeKey); err != nil {
return fmt.Errorf("failed to load %s from shared config, %s, %v",
ec2MetadataServiceEndpointModeKey, file.Filename, err)
}
updateString(&cfg.EC2IMDSEndpoint, section, ec2MetadataServiceEndpointKey)
}
updateString(&cfg.CredentialProcess, section, credentialProcessKey)
@ -321,6 +386,22 @@ func (cfg *sharedConfig) setFromIniFile(profile string, file sharedConfigFile, e
return nil
}
func updateEC2MetadataServiceEndpointMode(endpointMode *endpoints.EC2IMDSEndpointModeState, section ini.Section, key string) error {
if !section.Has(key) {
return nil
}
value := section.String(key)
return endpointMode.SetFromString(value)
}
func (cfg *sharedConfig) validateCredentialsConfig(profile string) error {
if err := cfg.validateCredentialsRequireARN(profile); err != nil {
return err
}
return nil
}
func (cfg *sharedConfig) validateCredentialsRequireARN(profile string) error {
var credSource string
@ -357,12 +438,43 @@ func (cfg *sharedConfig) validateCredentialType() error {
return nil
}
func (cfg *sharedConfig) validateSSOConfiguration() error {
if !cfg.hasSSOConfiguration() {
return nil
}
var missing []string
if len(cfg.SSOAccountID) == 0 {
missing = append(missing, ssoAccountIDKey)
}
if len(cfg.SSORegion) == 0 {
missing = append(missing, ssoRegionKey)
}
if len(cfg.SSORoleName) == 0 {
missing = append(missing, ssoRoleNameKey)
}
if len(cfg.SSOStartURL) == 0 {
missing = append(missing, ssoStartURL)
}
if len(missing) > 0 {
return fmt.Errorf("profile %q is configured to use SSO but is missing required configuration: %s",
cfg.Profile, strings.Join(missing, ", "))
}
return nil
}
func (cfg *sharedConfig) hasCredentials() bool {
switch {
case len(cfg.SourceProfileName) != 0:
case len(cfg.CredentialSource) != 0:
case len(cfg.CredentialProcess) != 0:
case len(cfg.WebIdentityTokenFile) != 0:
case cfg.hasSSOConfiguration():
case cfg.Creds.HasKeys():
default:
return false
@ -376,6 +488,10 @@ func (cfg *sharedConfig) clearCredentialOptions() {
cfg.CredentialProcess = ""
cfg.WebIdentityTokenFile = ""
cfg.Creds = credentials.Value{}
cfg.SSOAccountID = ""
cfg.SSORegion = ""
cfg.SSORoleName = ""
cfg.SSOStartURL = ""
}
func (cfg *sharedConfig) clearAssumeRoleOptions() {
@ -386,6 +502,18 @@ func (cfg *sharedConfig) clearAssumeRoleOptions() {
cfg.SourceProfileName = ""
}
func (cfg *sharedConfig) hasSSOConfiguration() bool {
switch {
case len(cfg.SSOAccountID) != 0:
case len(cfg.SSORegion) != 0:
case len(cfg.SSORoleName) != 0:
case len(cfg.SSOStartURL) != 0:
default:
return false
}
return true
}
func oneOrNone(bs ...bool) bool {
var count int

View file

@ -34,23 +34,23 @@ func (m mapRule) IsValid(value string) bool {
return ok
}
// whitelist is a generic rule for whitelisting
type whitelist struct {
// allowList is a generic rule for allow listing
type allowList struct {
rule
}
// IsValid for whitelist checks if the value is within the whitelist
func (w whitelist) IsValid(value string) bool {
// IsValid for allow list checks if the value is within the allow list
func (w allowList) IsValid(value string) bool {
return w.rule.IsValid(value)
}
// blacklist is a generic rule for blacklisting
type blacklist struct {
// excludeList is a generic rule for exclude listing
type excludeList struct {
rule
}
// IsValid for whitelist checks if the value is within the whitelist
func (b blacklist) IsValid(value string) bool {
// IsValid for exclude list checks if the value is within the exclude list
func (b excludeList) IsValid(value string) bool {
return !b.rule.IsValid(value)
}

View file

@ -0,0 +1,14 @@
//go:build !go1.7
// +build !go1.7
package v4
import (
"net/http"
"github.com/aws/aws-sdk-go/aws"
)
func requestContext(r *http.Request) aws.Context {
return aws.BackgroundContext()
}

View file

@ -0,0 +1,14 @@
//go:build go1.7
// +build go1.7
package v4
import (
"net/http"
"github.com/aws/aws-sdk-go/aws"
)
func requestContext(r *http.Request) aws.Context {
return r.Context()
}

View file

@ -1,3 +1,4 @@
//go:build go1.5
// +build go1.5
package v4

View file

@ -90,7 +90,7 @@ const (
)
var ignoredHeaders = rules{
blacklist{
excludeList{
mapRule{
authorizationHeader: struct{}{},
"User-Agent": struct{}{},
@ -99,9 +99,9 @@ var ignoredHeaders = rules{
},
}
// requiredSignedHeaders is a whitelist for build canonical headers.
// requiredSignedHeaders is a allow list for build canonical headers.
var requiredSignedHeaders = rules{
whitelist{
allowList{
mapRule{
"Cache-Control": struct{}{},
"Content-Disposition": struct{}{},
@ -145,12 +145,13 @@ var requiredSignedHeaders = rules{
},
},
patterns{"X-Amz-Meta-"},
patterns{"X-Amz-Object-Lock-"},
}
// allowedHoisting is a whitelist for build query headers. The boolean value
// allowedHoisting is a allow list for build query headers. The boolean value
// represents whether or not it is a pattern.
var allowedQueryHoisting = inclusiveRules{
blacklist{requiredSignedHeaders},
excludeList{requiredSignedHeaders},
patterns{"X-Amz-"},
}
@ -340,7 +341,7 @@ func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, regi
}
var err error
ctx.credValues, err = v4.Credentials.Get()
ctx.credValues, err = v4.Credentials.GetWithContext(requestContext(r))
if err != nil {
return http.Header{}, err
}
@ -417,7 +418,7 @@ var SignRequestHandler = request.NamedHandler{
// request handler should only be used with the SDK's built in service client's
// API operation requests.
//
// This function should not be used on its on its own, but in conjunction with
// This function should not be used on its own, but in conjunction with
// an AWS service client's API operation call. To sign a standalone request
// not created by a service client's API operation method use the "Sign" or
// "Presign" functions of the "Signer" type.
@ -689,9 +690,12 @@ func (ctx *signingCtx) buildBodyDigest() error {
if hash == "" {
includeSHA256Header := ctx.unsignedPayload ||
ctx.ServiceName == "s3" ||
ctx.ServiceName == "s3-object-lambda" ||
ctx.ServiceName == "glacier"
s3Presign := ctx.isPresign && ctx.ServiceName == "s3"
s3Presign := ctx.isPresign &&
(ctx.ServiceName == "s3" ||
ctx.ServiceName == "s3-object-lambda")
if ctx.unsignedPayload || s3Presign {
hash = "UNSIGNED-PAYLOAD"

View file

@ -239,3 +239,26 @@ func (es errors) Error() string {
return strings.Join(parts, "\n")
}
// CopySeekableBody copies the seekable body to an io.Writer
func CopySeekableBody(dst io.Writer, src io.ReadSeeker) (int64, error) {
curPos, err := src.Seek(0, sdkio.SeekCurrent)
if err != nil {
return 0, err
}
// copy errors may be assumed to be from the body.
n, err := io.Copy(dst, src)
if err != nil {
return n, err
}
// seek back to the first position after reading to reset
// the body for transmission.
_, err = src.Seek(curPos, sdkio.SeekStart)
if err != nil {
return n, err
}
return n, nil
}

View file

@ -1,3 +1,4 @@
//go:build go1.8
// +build go1.8
package aws

View file

@ -1,3 +1,4 @@
//go:build !go1.8
// +build !go1.8
package aws

View file

@ -5,4 +5,4 @@ package aws
const SDKName = "aws-sdk-go"
// SDKVersion is the version of this SDK
const SDKVersion = "1.28.0"
const SDKVersion = "1.40.41"

View file

@ -0,0 +1,41 @@
//go:build !go1.7
// +build !go1.7
package context
import "time"
// An emptyCtx is a copy of the Go 1.7 context.emptyCtx type. This is copied to
// provide a 1.6 and 1.5 safe version of context that is compatible with Go
// 1.7's Context.
//
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case BackgroundCtx:
return "aws.BackgroundContext"
}
return "unknown empty Context"
}
// BackgroundCtx is the common base context.
var BackgroundCtx = new(emptyCtx)

View file

@ -13,17 +13,30 @@
// }
//
// Below is the BNF that describes this parser
// Grammar:
// stmt -> value stmt'
// stmt' -> epsilon | op stmt
// value -> number | string | boolean | quoted_string
// Grammar:
// stmt -> section | stmt'
// stmt' -> epsilon | expr
// expr -> value (stmt)* | equal_expr (stmt)*
// equal_expr -> value ( ':' | '=' ) equal_expr'
// equal_expr' -> number | string | quoted_string
// quoted_string -> " quoted_string'
// quoted_string' -> string quoted_string_end
// quoted_string_end -> "
//
// section -> [ section'
// section' -> value section_close
// section_close -> ]
// section -> [ section'
// section' -> section_value section_close
// section_value -> number | string_subset | boolean | quoted_string_subset
// quoted_string_subset -> " quoted_string_subset'
// quoted_string_subset' -> string_subset quoted_string_end
// quoted_string_subset -> "
// section_close -> ]
//
// SkipState will skip (NL WS)+
// value -> number | string_subset | boolean
// string -> ? UTF-8 Code-Points except '\n' (U+000A) and '\r\n' (U+000D U+000A) ?
// string_subset -> ? Code-points excepted by <string> grammar except ':' (U+003A), '=' (U+003D), '[' (U+005B), and ']' (U+005D) ?
//
// comment -> # comment' | ; comment'
// comment' -> epsilon | value
// SkipState will skip (NL WS)+
//
// comment -> # comment' | ; comment'
// comment' -> epsilon | value
package ini

View file

@ -1,3 +1,4 @@
//go:build gofuzz
// +build gofuzz
package ini

View file

@ -5,9 +5,12 @@ import (
"io"
)
// ParseState represents the current state of the parser.
type ParseState uint
// State enums for the parse table
const (
InvalidState = iota
InvalidState ParseState = iota
// stmt -> value stmt'
StatementState
// stmt' -> MarkComplete | op stmt
@ -36,8 +39,8 @@ const (
)
// parseTable is a state machine to dictate the grammar above.
var parseTable = map[ASTKind]map[TokenType]int{
ASTKindStart: map[TokenType]int{
var parseTable = map[ASTKind]map[TokenType]ParseState{
ASTKindStart: {
TokenLit: StatementState,
TokenSep: OpenScopeState,
TokenWS: SkipTokenState,
@ -45,7 +48,7 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenComment: CommentState,
TokenNone: TerminalState,
},
ASTKindCommentStatement: map[TokenType]int{
ASTKindCommentStatement: {
TokenLit: StatementState,
TokenSep: OpenScopeState,
TokenWS: SkipTokenState,
@ -53,7 +56,7 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenComment: CommentState,
TokenNone: MarkCompleteState,
},
ASTKindExpr: map[TokenType]int{
ASTKindExpr: {
TokenOp: StatementPrimeState,
TokenLit: ValueState,
TokenSep: OpenScopeState,
@ -62,12 +65,15 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenComment: CommentState,
TokenNone: MarkCompleteState,
},
ASTKindEqualExpr: map[TokenType]int{
TokenLit: ValueState,
TokenWS: SkipTokenState,
TokenNL: SkipState,
ASTKindEqualExpr: {
TokenLit: ValueState,
TokenSep: ValueState,
TokenOp: ValueState,
TokenWS: SkipTokenState,
TokenNL: SkipState,
TokenNone: SkipState,
},
ASTKindStatement: map[TokenType]int{
ASTKindStatement: {
TokenLit: SectionState,
TokenSep: CloseScopeState,
TokenWS: SkipTokenState,
@ -75,9 +81,9 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenComment: CommentState,
TokenNone: MarkCompleteState,
},
ASTKindExprStatement: map[TokenType]int{
ASTKindExprStatement: {
TokenLit: ValueState,
TokenSep: OpenScopeState,
TokenSep: ValueState,
TokenOp: ValueState,
TokenWS: ValueState,
TokenNL: MarkCompleteState,
@ -85,14 +91,14 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenNone: TerminalState,
TokenComma: SkipState,
},
ASTKindSectionStatement: map[TokenType]int{
ASTKindSectionStatement: {
TokenLit: SectionState,
TokenOp: SectionState,
TokenSep: CloseScopeState,
TokenWS: SectionState,
TokenNL: SkipTokenState,
},
ASTKindCompletedSectionStatement: map[TokenType]int{
ASTKindCompletedSectionStatement: {
TokenWS: SkipTokenState,
TokenNL: SkipTokenState,
TokenLit: StatementState,
@ -100,7 +106,7 @@ var parseTable = map[ASTKind]map[TokenType]int{
TokenComment: CommentState,
TokenNone: MarkCompleteState,
},
ASTKindSkipStatement: map[TokenType]int{
ASTKindSkipStatement: {
TokenLit: StatementState,
TokenSep: OpenScopeState,
TokenWS: SkipTokenState,
@ -204,18 +210,6 @@ loop:
case ValueState:
// ValueState requires the previous state to either be an equal expression
// or an expression statement.
//
// This grammar occurs when the RHS is a number, word, or quoted string.
// equal_expr -> lit op equal_expr'
// equal_expr' -> number | string | quoted_string
// quoted_string -> " quoted_string'
// quoted_string' -> string quoted_string_end
// quoted_string_end -> "
//
// otherwise
// expr_stmt -> equal_expr (expr_stmt')*
// expr_stmt' -> ws S | op S | MarkComplete
// S -> equal_expr' expr_stmt'
switch k.Kind {
case ASTKindEqualExpr:
// assigning a value to some key
@ -242,7 +236,7 @@ loop:
}
children[len(children)-1] = rhs
k.SetChildren(children)
root.SetChildren(children)
stack.Push(k)
}

View file

@ -50,7 +50,10 @@ func (v *DefaultVisitor) VisitExpr(expr AST) error {
rhs := children[1]
if rhs.Root.Type() != TokenLit {
// The right-hand value side the equality expression is allowed to contain '[', ']', ':', '=' in the values.
// If the token is not either a literal or one of the token types that identifies those four additional
// tokens then error.
if !(rhs.Root.Type() == TokenLit || rhs.Root.Type() == TokenOp || rhs.Root.Type() == TokenSep) {
return NewParseError("unexpected token type")
}

View file

@ -19,23 +19,28 @@ func (a AccessPointARN) GetARN() arn.ARN {
// ParseAccessPointResource attempts to parse the ARN's resource as an
// AccessPoint resource.
//
// Supported Access point resource format:
// - Access point format: arn:{partition}:s3:{region}:{accountId}:accesspoint/{accesspointName}
// - example: arn.aws.s3.us-west-2.012345678901:accesspoint/myaccesspoint
//
func ParseAccessPointResource(a arn.ARN, resParts []string) (AccessPointARN, error) {
if len(a.Region) == 0 {
return AccessPointARN{}, InvalidARNError{a, "region not set"}
return AccessPointARN{}, InvalidARNError{ARN: a, Reason: "region not set"}
}
if len(a.AccountID) == 0 {
return AccessPointARN{}, InvalidARNError{a, "account-id not set"}
return AccessPointARN{}, InvalidARNError{ARN: a, Reason: "account-id not set"}
}
if len(resParts) == 0 {
return AccessPointARN{}, InvalidARNError{a, "resource-id not set"}
return AccessPointARN{}, InvalidARNError{ARN: a, Reason: "resource-id not set"}
}
if len(resParts) > 1 {
return AccessPointARN{}, InvalidARNError{a, "sub resource not supported"}
return AccessPointARN{}, InvalidARNError{ARN: a, Reason: "sub resource not supported"}
}
resID := resParts[0]
if len(strings.TrimSpace(resID)) == 0 {
return AccessPointARN{}, InvalidARNError{a, "resource-id not set"}
return AccessPointARN{}, InvalidARNError{ARN: a, Reason: "resource-id not set"}
}
return AccessPointARN{

View file

@ -1,11 +1,27 @@
package arn
import (
"fmt"
"strings"
"github.com/aws/aws-sdk-go/aws/arn"
)
var supportedServiceARN = []string{
"s3",
"s3-outposts",
"s3-object-lambda",
}
func isSupportedServiceARN(service string) bool {
for _, name := range supportedServiceARN {
if name == service {
return true
}
}
return false
}
// Resource provides the interfaces abstracting ARNs of specific resource
// types.
type Resource interface {
@ -25,13 +41,19 @@ func ParseResource(s string, resParser ResourceParser) (resARN Resource, err err
}
if len(a.Partition) == 0 {
return nil, InvalidARNError{a, "partition not set"}
return nil, InvalidARNError{ARN: a, Reason: "partition not set"}
}
if a.Service != "s3" {
return nil, InvalidARNError{a, "service is not S3"}
if !isSupportedServiceARN(a.Service) {
return nil, InvalidARNError{ARN: a, Reason: "service is not supported"}
}
if strings.HasPrefix(a.Region, "fips-") || strings.HasSuffix(a.Region, "-fips") {
return nil, InvalidARNError{ARN: a, Reason: "FIPS region not allowed in ARN"}
}
if len(a.Resource) == 0 {
return nil, InvalidARNError{a, "resource not set"}
return nil, InvalidARNError{ARN: a, Reason: "resource not set"}
}
return resParser(a)
@ -66,6 +88,7 @@ type InvalidARNError struct {
Reason string
}
// Error returns a string denoting the occurred InvalidARNError
func (e InvalidARNError) Error() string {
return "invalid Amazon S3 ARN, " + e.Reason + ", " + e.ARN.String()
return fmt.Sprintf("invalid Amazon %s ARN, %s, %s", e.ARN.Service, e.Reason, e.ARN.String())
}

View file

@ -0,0 +1,126 @@
package arn
import (
"strings"
"github.com/aws/aws-sdk-go/aws/arn"
)
// OutpostARN interface that should be satisfied by outpost ARNs
type OutpostARN interface {
Resource
GetOutpostID() string
}
// ParseOutpostARNResource will parse a provided ARNs resource using the appropriate ARN format
// and return a specific OutpostARN type
//
// Currently supported outpost ARN formats:
// * Outpost AccessPoint ARN format:
// - ARN format: arn:{partition}:s3-outposts:{region}:{accountId}:outpost/{outpostId}/accesspoint/{accesspointName}
// - example: arn:aws:s3-outposts:us-west-2:012345678901:outpost/op-1234567890123456/accesspoint/myaccesspoint
//
// * Outpost Bucket ARN format:
// - ARN format: arn:{partition}:s3-outposts:{region}:{accountId}:outpost/{outpostId}/bucket/{bucketName}
// - example: arn:aws:s3-outposts:us-west-2:012345678901:outpost/op-1234567890123456/bucket/mybucket
//
// Other outpost ARN formats may be supported and added in the future.
//
func ParseOutpostARNResource(a arn.ARN, resParts []string) (OutpostARN, error) {
if len(a.Region) == 0 {
return nil, InvalidARNError{ARN: a, Reason: "region not set"}
}
if len(a.AccountID) == 0 {
return nil, InvalidARNError{ARN: a, Reason: "account-id not set"}
}
// verify if outpost id is present and valid
if len(resParts) == 0 || len(strings.TrimSpace(resParts[0])) == 0 {
return nil, InvalidARNError{ARN: a, Reason: "outpost resource-id not set"}
}
// verify possible resource type exists
if len(resParts) < 3 {
return nil, InvalidARNError{
ARN: a, Reason: "incomplete outpost resource type. Expected bucket or access-point resource to be present",
}
}
// Since we know this is a OutpostARN fetch outpostID
outpostID := strings.TrimSpace(resParts[0])
switch resParts[1] {
case "accesspoint":
accesspointARN, err := ParseAccessPointResource(a, resParts[2:])
if err != nil {
return OutpostAccessPointARN{}, err
}
return OutpostAccessPointARN{
AccessPointARN: accesspointARN,
OutpostID: outpostID,
}, nil
case "bucket":
bucketName, err := parseBucketResource(a, resParts[2:])
if err != nil {
return nil, err
}
return OutpostBucketARN{
ARN: a,
BucketName: bucketName,
OutpostID: outpostID,
}, nil
default:
return nil, InvalidARNError{ARN: a, Reason: "unknown resource set for outpost ARN"}
}
}
// OutpostAccessPointARN represents outpost access point ARN.
type OutpostAccessPointARN struct {
AccessPointARN
OutpostID string
}
// GetOutpostID returns the outpost id of outpost access point arn
func (o OutpostAccessPointARN) GetOutpostID() string {
return o.OutpostID
}
// OutpostBucketARN represents the outpost bucket ARN.
type OutpostBucketARN struct {
arn.ARN
BucketName string
OutpostID string
}
// GetOutpostID returns the outpost id of outpost bucket arn
func (o OutpostBucketARN) GetOutpostID() string {
return o.OutpostID
}
// GetARN retrives the base ARN from outpost bucket ARN resource
func (o OutpostBucketARN) GetARN() arn.ARN {
return o.ARN
}
// parseBucketResource attempts to parse the ARN's bucket resource and retrieve the
// bucket resource id.
//
// parseBucketResource only parses the bucket resource id.
//
func parseBucketResource(a arn.ARN, resParts []string) (bucketName string, err error) {
if len(resParts) == 0 {
return bucketName, InvalidARNError{ARN: a, Reason: "bucket resource-id not set"}
}
if len(resParts) > 1 {
return bucketName, InvalidARNError{ARN: a, Reason: "sub resource not supported"}
}
bucketName = strings.TrimSpace(resParts[0])
if len(bucketName) == 0 {
return bucketName, InvalidARNError{ARN: a, Reason: "bucket resource-id not set"}
}
return bucketName, err
}

View file

@ -0,0 +1,15 @@
package arn
// S3ObjectLambdaARN represents an ARN for the s3-object-lambda service
type S3ObjectLambdaARN interface {
Resource
isS3ObjectLambdasARN()
}
// S3ObjectLambdaAccessPointARN is an S3ObjectLambdaARN for the Access Point resource type
type S3ObjectLambdaAccessPointARN struct {
AccessPointARN
}
func (s S3ObjectLambdaAccessPointARN) isS3ObjectLambdasARN() {}

View file

@ -0,0 +1,202 @@
package s3shared
import (
"fmt"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/internal/s3shared/arn"
)
const (
invalidARNErrorErrCode = "InvalidARNError"
configurationErrorErrCode = "ConfigurationError"
)
// InvalidARNError denotes the error for Invalid ARN
type InvalidARNError struct {
message string
resource arn.Resource
origErr error
}
// Error returns the InvalidARNError
func (e InvalidARNError) Error() string {
var extra string
if e.resource != nil {
extra = "ARN: " + e.resource.String()
}
return awserr.SprintError(e.Code(), e.Message(), extra, e.origErr)
}
// Code returns the invalid ARN error code
func (e InvalidARNError) Code() string {
return invalidARNErrorErrCode
}
// Message returns the message for Invalid ARN error
func (e InvalidARNError) Message() string {
return e.message
}
// OrigErr is the original error wrapped by Invalid ARN Error
func (e InvalidARNError) OrigErr() error {
return e.origErr
}
// NewInvalidARNError denotes invalid arn error
func NewInvalidARNError(resource arn.Resource, err error) InvalidARNError {
return InvalidARNError{
message: "invalid ARN",
origErr: err,
resource: resource,
}
}
// NewInvalidARNWithCustomEndpointError ARN not supported for custom clients endpoints
func NewInvalidARNWithCustomEndpointError(resource arn.Resource, err error) InvalidARNError {
return InvalidARNError{
message: "resource ARN not supported with custom client endpoints",
origErr: err,
resource: resource,
}
}
// NewInvalidARNWithUnsupportedPartitionError ARN not supported for the target partition
func NewInvalidARNWithUnsupportedPartitionError(resource arn.Resource, err error) InvalidARNError {
return InvalidARNError{
message: "resource ARN not supported for the target ARN partition",
origErr: err,
resource: resource,
}
}
// NewInvalidARNWithFIPSError ARN not supported for FIPS region
//
// Deprecated: FIPS will not appear in the ARN region component.
func NewInvalidARNWithFIPSError(resource arn.Resource, err error) InvalidARNError {
return InvalidARNError{
message: "resource ARN not supported for FIPS region",
resource: resource,
origErr: err,
}
}
// ConfigurationError is used to denote a client configuration error
type ConfigurationError struct {
message string
resource arn.Resource
clientPartitionID string
clientRegion string
origErr error
}
// Error returns the Configuration error string
func (e ConfigurationError) Error() string {
extra := fmt.Sprintf("ARN: %s, client partition: %s, client region: %s",
e.resource, e.clientPartitionID, e.clientRegion)
return awserr.SprintError(e.Code(), e.Message(), extra, e.origErr)
}
// Code returns configuration error's error-code
func (e ConfigurationError) Code() string {
return configurationErrorErrCode
}
// Message returns the configuration error message
func (e ConfigurationError) Message() string {
return e.message
}
// OrigErr is the original error wrapped by Configuration Error
func (e ConfigurationError) OrigErr() error {
return e.origErr
}
// NewClientPartitionMismatchError stub
func NewClientPartitionMismatchError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client partition does not match provided ARN partition",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewClientRegionMismatchError denotes cross region access error
func NewClientRegionMismatchError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client region does not match provided ARN region",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewFailedToResolveEndpointError denotes endpoint resolving error
func NewFailedToResolveEndpointError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "endpoint resolver failed to find an endpoint for the provided ARN region",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewClientConfiguredForFIPSError denotes client config error for unsupported cross region FIPS access
func NewClientConfiguredForFIPSError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client configured for fips but cross-region resource ARN provided",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewFIPSConfigurationError denotes a configuration error when a client or request is configured for FIPS
func NewFIPSConfigurationError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "use of ARN is not supported when client or request is configured for FIPS",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewClientConfiguredForAccelerateError denotes client config error for unsupported S3 accelerate
func NewClientConfiguredForAccelerateError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client configured for S3 Accelerate but is not supported with resource ARN",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewClientConfiguredForCrossRegionFIPSError denotes client config error for unsupported cross region FIPS request
func NewClientConfiguredForCrossRegionFIPSError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client configured for FIPS with cross-region enabled but is supported with cross-region resource ARN",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}
// NewClientConfiguredForDualStackError denotes client config error for unsupported S3 Dual-stack
func NewClientConfiguredForDualStackError(resource arn.Resource, clientPartitionID, clientRegion string, err error) ConfigurationError {
return ConfigurationError{
message: "client configured for S3 Dual-stack but is not supported with resource ARN",
origErr: err,
resource: resource,
clientPartitionID: clientPartitionID,
clientRegion: clientRegion,
}
}

View file

@ -0,0 +1,64 @@
package s3shared
import (
"strings"
"github.com/aws/aws-sdk-go/aws"
awsarn "github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/internal/s3shared/arn"
)
// ResourceRequest represents the request and arn resource
type ResourceRequest struct {
Resource arn.Resource
Request *request.Request
}
// ARN returns the resource ARN
func (r ResourceRequest) ARN() awsarn.ARN {
return r.Resource.GetARN()
}
// AllowCrossRegion returns a bool value to denote if S3UseARNRegion flag is set
func (r ResourceRequest) AllowCrossRegion() bool {
return aws.BoolValue(r.Request.Config.S3UseARNRegion)
}
// UseFIPS returns true if request config region is FIPS
func (r ResourceRequest) UseFIPS() bool {
return IsFIPS(aws.StringValue(r.Request.Config.Region))
}
// ResourceConfiguredForFIPS returns true if resource ARNs region is FIPS
//
// Deprecated: FIPS pseudo-regions will not be in the ARN
func (r ResourceRequest) ResourceConfiguredForFIPS() bool {
return IsFIPS(r.ARN().Region)
}
// IsCrossPartition returns true if client is configured for another partition, than
// the partition that resource ARN region resolves to.
func (r ResourceRequest) IsCrossPartition() bool {
return r.Request.ClientInfo.PartitionID != r.Resource.GetARN().Partition
}
// IsCrossRegion returns true if ARN region is different than client configured region
func (r ResourceRequest) IsCrossRegion() bool {
return IsCrossRegion(r.Request, r.Resource.GetARN().Region)
}
// HasCustomEndpoint returns true if custom client endpoint is provided
func (r ResourceRequest) HasCustomEndpoint() bool {
return len(aws.StringValue(r.Request.Config.Endpoint)) > 0
}
// IsFIPS returns true if region is a fips region
func IsFIPS(clientRegion string) bool {
return strings.HasPrefix(clientRegion, "fips-") || strings.HasSuffix(clientRegion, "-fips")
}
// IsCrossRegion returns true if request signing region is not same as configured region
func IsCrossRegion(req *request.Request, otherRegion string) bool {
return req.ClientInfo.SigningRegion != otherRegion
}

View file

@ -1,3 +1,4 @@
//go:build !go1.7
// +build !go1.7
package sdkio

View file

@ -1,3 +1,4 @@
//go:build go1.7
// +build go1.7
package sdkio

View file

@ -1,3 +1,4 @@
//go:build go1.10
// +build go1.10
package sdkmath

View file

@ -1,3 +1,4 @@
//go:build !go1.10
// +build !go1.10
package sdkmath

View file

@ -1,3 +1,4 @@
//go:build go1.6
// +build go1.6
package sdkrand

View file

@ -1,3 +1,4 @@
//go:build !go1.6
// +build !go1.6
package sdkrand

View file

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,120 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package singleflight provides a duplicate function call suppression
// mechanism.
package singleflight
import "sync"
// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup
// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
val interface{}
err error
// forgotten indicates whether Forget was called with this call's key
// while the call was still in flight.
forgotten bool
// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
dups int
chans []chan<- Result
}
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}
// Result holds the results of Do, so they can be passed
// on a channel.
type Result struct {
Val interface{}
Err error
Shared bool
}
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
g.mu.Unlock()
c.wg.Wait()
return c.val, c.err, true
}
c := new(call)
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}
// DoChan is like Do but returns a channel that will receive the
// results when they are ready.
func (g *Group) DoChan(key string, fn func() (interface{}, error)) <-chan Result {
ch := make(chan Result, 1)
g.mu.Lock()
if g.m == nil {
g.m = make(map[string]*call)
}
if c, ok := g.m[key]; ok {
c.dups++
c.chans = append(c.chans, ch)
g.mu.Unlock()
return ch
}
c := &call{chans: []chan<- Result{ch}}
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
go g.doCall(c, key, fn)
return ch
}
// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
c.val, c.err = fn()
c.wg.Done()
g.mu.Lock()
if !c.forgotten {
delete(g.m, key)
}
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
g.mu.Unlock()
}
// Forget tells the singleflight to forget about a key. Future calls
// to Do for this key will call the function rather than waiting for
// an earlier call to complete.
func (g *Group) Forget(key string) {
g.mu.Lock()
if c, ok := g.m[key]; ok {
c.forgotten = true
}
delete(g.m, key)
g.mu.Unlock()
}

View file

@ -0,0 +1,53 @@
package checksum
import (
"crypto/md5"
"encoding/base64"
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
)
const contentMD5Header = "Content-Md5"
// AddBodyContentMD5Handler computes and sets the HTTP Content-MD5 header for requests that
// require it.
func AddBodyContentMD5Handler(r *request.Request) {
// if Content-MD5 header is already present, return
if v := r.HTTPRequest.Header.Get(contentMD5Header); len(v) != 0 {
return
}
// if S3DisableContentMD5Validation flag is set, return
if aws.BoolValue(r.Config.S3DisableContentMD5Validation) {
return
}
// if request is presigned, return
if r.IsPresigned() {
return
}
// if body is not seekable, return
if !aws.IsReaderSeekable(r.Body) {
if r.Config.Logger != nil {
r.Config.Logger.Log(fmt.Sprintf(
"Unable to compute Content-MD5 for unseekable body, S3.%s",
r.Operation.Name))
}
return
}
h := md5.New()
if _, err := aws.CopySeekableBody(h, r.Body); err != nil {
r.Error = awserr.New("ContentMD5", "failed to compute body MD5", err)
return
}
// encode the md5 checksum in base64 and set the request header.
v := base64.StdEncoding.EncodeToString(h.Sum(nil))
r.HTTPRequest.Header.Set(contentMD5Header, v)
}

View file

@ -5,6 +5,10 @@ import (
"sync"
)
// InputWriterCloseErrorCode is used to denote an error occurred
// while closing the event stream input writer.
const InputWriterCloseErrorCode = "EventStreamInputWriterCloseError"
type messageError struct {
code string
msg string

View file

@ -69,10 +69,23 @@ func (r *EventReader) ReadEvent() (event interface{}, err error) {
case ErrorMessageType:
return nil, r.unmarshalErrorMessage(msg)
default:
return nil, fmt.Errorf("unknown eventstream message type, %v", typ)
return nil, &UnknownMessageTypeError{
Type: typ, Message: msg.Clone(),
}
}
}
// UnknownMessageTypeError provides an error when a message is received from
// the stream, but the reader is unable to determine what kind of message it is.
type UnknownMessageTypeError struct {
Type string
Message eventstream.Message
}
func (e *UnknownMessageTypeError) Error() string {
return "unknown eventstream message type, " + e.Type
}
func (r *EventReader) unmarshalEventMessage(
msg eventstream.Message,
) (event interface{}, err error) {

View file

@ -61,49 +61,3 @@ func (w *EventWriter) marshal(event Marshaler) (eventstream.Message, error) {
msg.Headers.Set(EventTypeHeader, eventstream.StringValue(eventType))
return msg, nil
}
//type EventEncoder struct {
// encoder Encoder
// ppayloadMarshaler protocol.PayloadMarshaler
// eventTypeFor func(Marshaler) (string, error)
//}
//
//func (e EventEncoder) Encode(event Marshaler) error {
// msg, err := e.marshal(event)
// if err != nil {
// return err
// }
//
// return w.encoder.Encode(msg)
//}
//
//func (e EventEncoder) marshal(event Marshaler) (eventstream.Message, error) {
// eventType, err := w.eventTypeFor(event)
// if err != nil {
// return eventstream.Message{}, err
// }
//
// msg, err := event.MarshalEvent(w.payloadMarshaler)
// if err != nil {
// return eventstream.Message{}, err
// }
//
// msg.Headers.Set(EventTypeHeader, eventstream.StringValue(eventType))
// return msg, nil
//}
//
//func (w *EventWriter) marshal(event Marshaler) (eventstream.Message, error) {
// eventType, err := w.eventTypeFor(event)
// if err != nil {
// return eventstream.Message{}, err
// }
//
// msg, err := event.MarshalEvent(w.payloadMarshaler)
// if err != nil {
// return eventstream.Message{}, err
// }
//
// msg.Headers.Set(EventTypeHeader, eventstream.StringValue(eventType))
// return msg, nil
//}
//

View file

@ -52,6 +52,15 @@ func (hs *Headers) Del(name string) {
}
}
// Clone returns a deep copy of the headers
func (hs Headers) Clone() Headers {
o := make(Headers, 0, len(hs))
for _, h := range hs {
o.Set(h.Name, h.Value)
}
return o
}
func decodeHeaders(r io.Reader) (Headers, error) {
hs := Headers{}

View file

@ -57,6 +57,20 @@ func (m *Message) rawMessage() (rawMessage, error) {
return raw, nil
}
// Clone returns a deep copy of the message.
func (m Message) Clone() Message {
var payload []byte
if m.Payload != nil {
payload = make([]byte, len(m.Payload))
copy(payload, m.Payload)
}
return Message{
Headers: m.Headers.Clone(),
Payload: payload,
}
}
type messagePrelude struct {
Length uint32
HeadersLen uint32

View file

@ -1,9 +1,10 @@
package protocol
import (
"strings"
"github.com/aws/aws-sdk-go/aws/request"
"net"
"strconv"
"strings"
)
// ValidateEndpointHostHandler is a request handler that will validate the
@ -22,8 +23,26 @@ var ValidateEndpointHostHandler = request.NamedHandler{
// 3986 host. Returns error if the host is not valid.
func ValidateEndpointHost(opName, host string) error {
paramErrs := request.ErrInvalidParams{Context: opName}
labels := strings.Split(host, ".")
var hostname string
var port string
var err error
if strings.Contains(host, ":") {
hostname, port, err = net.SplitHostPort(host)
if err != nil {
paramErrs.Add(request.NewErrParamFormat("endpoint", err.Error(), host))
}
if !ValidPortNumber(port) {
paramErrs.Add(request.NewErrParamFormat("endpoint port number", "[0-65535]", port))
}
} else {
hostname = host
}
labels := strings.Split(hostname, ".")
for i, label := range labels {
if i == len(labels)-1 && len(label) == 0 {
// Allow trailing dot for FQDN hosts.
@ -36,7 +55,11 @@ func ValidateEndpointHost(opName, host string) error {
}
}
if len(host) > 255 {
if len(hostname) == 0 {
paramErrs.Add(request.NewErrParamMinLen("endpoint host", 1))
}
if len(hostname) > 255 {
paramErrs.Add(request.NewErrParamMaxLen(
"endpoint host", 255, host,
))
@ -66,3 +89,16 @@ func ValidHostLabel(label string) bool {
return true
}
// ValidPortNumber return if the port is valid RFC 3986 port
func ValidPortNumber(port string) bool {
i, err := strconv.Atoi(port)
if err != nil {
return false
}
if i < 0 || i > 65535 {
return false
}
return true
}

View file

@ -6,7 +6,9 @@ import (
"encoding/json"
"fmt"
"io"
"math/big"
"reflect"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
@ -14,6 +16,8 @@ import (
"github.com/aws/aws-sdk-go/private/protocol"
)
var millisecondsFloat = new(big.Float).SetInt64(1e3)
// UnmarshalJSONError unmarshal's the reader's JSON document into the passed in
// type. The value to unmarshal the json document into must be a pointer to the
// type.
@ -38,17 +42,42 @@ func UnmarshalJSONError(v interface{}, stream io.Reader) error {
func UnmarshalJSON(v interface{}, stream io.Reader) error {
var out interface{}
err := json.NewDecoder(stream).Decode(&out)
decoder := json.NewDecoder(stream)
decoder.UseNumber()
err := decoder.Decode(&out)
if err == io.EOF {
return nil
} else if err != nil {
return err
}
return unmarshalAny(reflect.ValueOf(v), out, "")
return unmarshaler{}.unmarshalAny(reflect.ValueOf(v), out, "")
}
func unmarshalAny(value reflect.Value, data interface{}, tag reflect.StructTag) error {
// UnmarshalJSONCaseInsensitive reads a stream and unmarshals the result into the
// object v. Ignores casing for structure members.
func UnmarshalJSONCaseInsensitive(v interface{}, stream io.Reader) error {
var out interface{}
decoder := json.NewDecoder(stream)
decoder.UseNumber()
err := decoder.Decode(&out)
if err == io.EOF {
return nil
} else if err != nil {
return err
}
return unmarshaler{
caseInsensitive: true,
}.unmarshalAny(reflect.ValueOf(v), out, "")
}
type unmarshaler struct {
caseInsensitive bool
}
func (u unmarshaler) unmarshalAny(value reflect.Value, data interface{}, tag reflect.StructTag) error {
vtype := value.Type()
if vtype.Kind() == reflect.Ptr {
vtype = vtype.Elem() // check kind of actual element type
@ -80,17 +109,17 @@ func unmarshalAny(value reflect.Value, data interface{}, tag reflect.StructTag)
if field, ok := vtype.FieldByName("_"); ok {
tag = field.Tag
}
return unmarshalStruct(value, data, tag)
return u.unmarshalStruct(value, data, tag)
case "list":
return unmarshalList(value, data, tag)
return u.unmarshalList(value, data, tag)
case "map":
return unmarshalMap(value, data, tag)
return u.unmarshalMap(value, data, tag)
default:
return unmarshalScalar(value, data, tag)
return u.unmarshalScalar(value, data, tag)
}
}
func unmarshalStruct(value reflect.Value, data interface{}, tag reflect.StructTag) error {
func (u unmarshaler) unmarshalStruct(value reflect.Value, data interface{}, tag reflect.StructTag) error {
if data == nil {
return nil
}
@ -114,7 +143,7 @@ func unmarshalStruct(value reflect.Value, data interface{}, tag reflect.StructTa
// unwrap any payloads
if payload := tag.Get("payload"); payload != "" {
field, _ := t.FieldByName(payload)
return unmarshalAny(value.FieldByName(payload), data, field.Tag)
return u.unmarshalAny(value.FieldByName(payload), data, field.Tag)
}
for i := 0; i < t.NumField(); i++ {
@ -128,9 +157,19 @@ func unmarshalStruct(value reflect.Value, data interface{}, tag reflect.StructTa
if locName := field.Tag.Get("locationName"); locName != "" {
name = locName
}
if u.caseInsensitive {
if _, ok := mapData[name]; !ok {
// Fallback to uncased name search if the exact name didn't match.
for kn, v := range mapData {
if strings.EqualFold(kn, name) {
mapData[name] = v
}
}
}
}
member := value.FieldByIndex(field.Index)
err := unmarshalAny(member, mapData[name], field.Tag)
err := u.unmarshalAny(member, mapData[name], field.Tag)
if err != nil {
return err
}
@ -138,7 +177,7 @@ func unmarshalStruct(value reflect.Value, data interface{}, tag reflect.StructTa
return nil
}
func unmarshalList(value reflect.Value, data interface{}, tag reflect.StructTag) error {
func (u unmarshaler) unmarshalList(value reflect.Value, data interface{}, tag reflect.StructTag) error {
if data == nil {
return nil
}
@ -153,7 +192,7 @@ func unmarshalList(value reflect.Value, data interface{}, tag reflect.StructTag)
}
for i, c := range listData {
err := unmarshalAny(value.Index(i), c, "")
err := u.unmarshalAny(value.Index(i), c, "")
if err != nil {
return err
}
@ -162,7 +201,7 @@ func unmarshalList(value reflect.Value, data interface{}, tag reflect.StructTag)
return nil
}
func unmarshalMap(value reflect.Value, data interface{}, tag reflect.StructTag) error {
func (u unmarshaler) unmarshalMap(value reflect.Value, data interface{}, tag reflect.StructTag) error {
if data == nil {
return nil
}
@ -179,14 +218,14 @@ func unmarshalMap(value reflect.Value, data interface{}, tag reflect.StructTag)
kvalue := reflect.ValueOf(k)
vvalue := reflect.New(value.Type().Elem()).Elem()
unmarshalAny(vvalue, v, "")
u.unmarshalAny(vvalue, v, "")
value.SetMapIndex(kvalue, vvalue)
}
return nil
}
func unmarshalScalar(value reflect.Value, data interface{}, tag reflect.StructTag) error {
func (u unmarshaler) unmarshalScalar(value reflect.Value, data interface{}, tag reflect.StructTag) error {
switch d := data.(type) {
case nil:
@ -222,16 +261,31 @@ func unmarshalScalar(value reflect.Value, data interface{}, tag reflect.StructTa
default:
return fmt.Errorf("unsupported value: %v (%s)", value.Interface(), value.Type())
}
case float64:
case json.Number:
switch value.Interface().(type) {
case *int64:
di := int64(d)
// Retain the old behavior where we would just truncate the float64
// calling d.Int64() here could cause an invalid syntax error due to the usage of strconv.ParseInt
f, err := d.Float64()
if err != nil {
return err
}
di := int64(f)
value.Set(reflect.ValueOf(&di))
case *float64:
value.Set(reflect.ValueOf(&d))
f, err := d.Float64()
if err != nil {
return err
}
value.Set(reflect.ValueOf(&f))
case *time.Time:
// Time unmarshaled from a float64 can only be epoch seconds
t := time.Unix(int64(d), 0).UTC()
float, ok := new(big.Float).SetString(d.String())
if !ok {
return fmt.Errorf("unsupported float time representation: %v", d.String())
}
float = float.Mul(float, millisecondsFloat)
ms, _ := float.Int64()
t := time.Unix(0, ms*1e6).UTC()
value.Set(reflect.ValueOf(&t))
default:
return fmt.Errorf("unsupported value: %v (%s)", value.Interface(), value.Type())

View file

@ -0,0 +1,88 @@
// Package jsonrpc provides JSON RPC utilities for serialization of AWS
// requests and responses.
package jsonrpc
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/input/json.json build_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/output/json.json unmarshal_test.go
import (
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
"github.com/aws/aws-sdk-go/private/protocol/rest"
)
var emptyJSON = []byte("{}")
// BuildHandler is a named request handler for building jsonrpc protocol
// requests
var BuildHandler = request.NamedHandler{
Name: "awssdk.jsonrpc.Build",
Fn: Build,
}
// UnmarshalHandler is a named request handler for unmarshaling jsonrpc
// protocol requests
var UnmarshalHandler = request.NamedHandler{
Name: "awssdk.jsonrpc.Unmarshal",
Fn: Unmarshal,
}
// UnmarshalMetaHandler is a named request handler for unmarshaling jsonrpc
// protocol request metadata
var UnmarshalMetaHandler = request.NamedHandler{
Name: "awssdk.jsonrpc.UnmarshalMeta",
Fn: UnmarshalMeta,
}
// Build builds a JSON payload for a JSON RPC request.
func Build(req *request.Request) {
var buf []byte
var err error
if req.ParamsFilled() {
buf, err = jsonutil.BuildJSON(req.Params)
if err != nil {
req.Error = awserr.New(request.ErrCodeSerialization, "failed encoding JSON RPC request", err)
return
}
} else {
buf = emptyJSON
}
if req.ClientInfo.TargetPrefix != "" || string(buf) != "{}" {
req.SetBufferBody(buf)
}
if req.ClientInfo.TargetPrefix != "" {
target := req.ClientInfo.TargetPrefix + "." + req.Operation.Name
req.HTTPRequest.Header.Add("X-Amz-Target", target)
}
// Only set the content type if one is not already specified and an
// JSONVersion is specified.
if ct, v := req.HTTPRequest.Header.Get("Content-Type"), req.ClientInfo.JSONVersion; len(ct) == 0 && len(v) != 0 {
jsonVersion := req.ClientInfo.JSONVersion
req.HTTPRequest.Header.Set("Content-Type", "application/x-amz-json-"+jsonVersion)
}
}
// Unmarshal unmarshals a response for a JSON RPC service.
func Unmarshal(req *request.Request) {
defer req.HTTPResponse.Body.Close()
if req.DataFilled() {
err := jsonutil.UnmarshalJSON(req.Data, req.HTTPResponse.Body)
if err != nil {
req.Error = awserr.NewRequestFailure(
awserr.New(request.ErrCodeSerialization, "failed decoding JSON RPC response", err),
req.HTTPResponse.StatusCode,
req.RequestID,
)
}
}
return
}
// UnmarshalMeta unmarshals headers from a response for a JSON RPC service.
func UnmarshalMeta(req *request.Request) {
rest.UnmarshalMeta(req)
}

View file

@ -0,0 +1,107 @@
package jsonrpc
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/private/protocol"
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
)
// UnmarshalTypedError provides unmarshaling errors API response errors
// for both typed and untyped errors.
type UnmarshalTypedError struct {
exceptions map[string]func(protocol.ResponseMetadata) error
}
// NewUnmarshalTypedError returns an UnmarshalTypedError initialized for the
// set of exception names to the error unmarshalers
func NewUnmarshalTypedError(exceptions map[string]func(protocol.ResponseMetadata) error) *UnmarshalTypedError {
return &UnmarshalTypedError{
exceptions: exceptions,
}
}
// UnmarshalError attempts to unmarshal the HTTP response error as a known
// error type. If unable to unmarshal the error type, the generic SDK error
// type will be used.
func (u *UnmarshalTypedError) UnmarshalError(
resp *http.Response,
respMeta protocol.ResponseMetadata,
) (error, error) {
var buf bytes.Buffer
var jsonErr jsonErrorResponse
teeReader := io.TeeReader(resp.Body, &buf)
err := jsonutil.UnmarshalJSONError(&jsonErr, teeReader)
if err != nil {
return nil, err
}
body := ioutil.NopCloser(&buf)
// Code may be separated by hash(#), with the last element being the code
// used by the SDK.
codeParts := strings.SplitN(jsonErr.Code, "#", 2)
code := codeParts[len(codeParts)-1]
msg := jsonErr.Message
if fn, ok := u.exceptions[code]; ok {
// If exception code is know, use associated constructor to get a value
// for the exception that the JSON body can be unmarshaled into.
v := fn(respMeta)
err := jsonutil.UnmarshalJSONCaseInsensitive(v, body)
if err != nil {
return nil, err
}
return v, nil
}
// fallback to unmodeled generic exceptions
return awserr.NewRequestFailure(
awserr.New(code, msg, nil),
respMeta.StatusCode,
respMeta.RequestID,
), nil
}
// UnmarshalErrorHandler is a named request handler for unmarshaling jsonrpc
// protocol request errors
var UnmarshalErrorHandler = request.NamedHandler{
Name: "awssdk.jsonrpc.UnmarshalError",
Fn: UnmarshalError,
}
// UnmarshalError unmarshals an error response for a JSON RPC service.
func UnmarshalError(req *request.Request) {
defer req.HTTPResponse.Body.Close()
var jsonErr jsonErrorResponse
err := jsonutil.UnmarshalJSONError(&jsonErr, req.HTTPResponse.Body)
if err != nil {
req.Error = awserr.NewRequestFailure(
awserr.New(request.ErrCodeSerialization,
"failed to unmarshal error message", err),
req.HTTPResponse.StatusCode,
req.RequestID,
)
return
}
codes := strings.SplitN(jsonErr.Code, "#", 2)
req.Error = awserr.NewRequestFailure(
awserr.New(codes[len(codes)-1], jsonErr.Message, nil),
req.HTTPResponse.StatusCode,
req.RequestID,
)
}
type jsonErrorResponse struct {
Code string `json:"__type"`
Message string `json:"message"`
}

View file

@ -1,7 +1,7 @@
// Package query provides serialization of AWS query requests, and responses.
package query
//go:generate go run -tags codegen ../../../models/protocol_tests/generate.go ../../../models/protocol_tests/input/query.json build_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/input/query.json build_test.go
import (
"net/url"

View file

@ -1,6 +1,6 @@
package query
//go:generate go run -tags codegen ../../../models/protocol_tests/generate.go ../../../models/protocol_tests/output/query.json unmarshal_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/output/query.json unmarshal_test.go
import (
"encoding/xml"

View file

@ -98,7 +98,7 @@ func buildLocationElements(r *request.Request, v reflect.Value, buildGETQuery bo
// Support the ability to customize values to be marshaled as a
// blob even though they were modeled as a string. Required for S3
// API operations like SSECustomerKey is modeled as stirng but
// API operations like SSECustomerKey is modeled as string but
// required to be base64 encoded in request.
if field.Tag.Get("marshal-as") == "blob" {
m = m.Convert(byteSliceType)

View file

@ -0,0 +1,59 @@
// Package restjson provides RESTful JSON serialization of AWS
// requests and responses.
package restjson
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/input/rest-json.json build_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/output/rest-json.json unmarshal_test.go
import (
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/private/protocol/jsonrpc"
"github.com/aws/aws-sdk-go/private/protocol/rest"
)
// BuildHandler is a named request handler for building restjson protocol
// requests
var BuildHandler = request.NamedHandler{
Name: "awssdk.restjson.Build",
Fn: Build,
}
// UnmarshalHandler is a named request handler for unmarshaling restjson
// protocol requests
var UnmarshalHandler = request.NamedHandler{
Name: "awssdk.restjson.Unmarshal",
Fn: Unmarshal,
}
// UnmarshalMetaHandler is a named request handler for unmarshaling restjson
// protocol request metadata
var UnmarshalMetaHandler = request.NamedHandler{
Name: "awssdk.restjson.UnmarshalMeta",
Fn: UnmarshalMeta,
}
// Build builds a request for the REST JSON protocol.
func Build(r *request.Request) {
rest.Build(r)
if t := rest.PayloadType(r.Params); t == "structure" || t == "" {
if v := r.HTTPRequest.Header.Get("Content-Type"); len(v) == 0 {
r.HTTPRequest.Header.Set("Content-Type", "application/json")
}
jsonrpc.Build(r)
}
}
// Unmarshal unmarshals a response body for the REST JSON protocol.
func Unmarshal(r *request.Request) {
if t := rest.PayloadType(r.Data); t == "structure" || t == "" {
jsonrpc.Unmarshal(r)
} else {
rest.Unmarshal(r)
}
}
// UnmarshalMeta unmarshals response headers for the REST JSON protocol.
func UnmarshalMeta(r *request.Request) {
rest.UnmarshalMeta(r)
}

View file

@ -0,0 +1,134 @@
package restjson
import (
"bytes"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/private/protocol"
"github.com/aws/aws-sdk-go/private/protocol/json/jsonutil"
"github.com/aws/aws-sdk-go/private/protocol/rest"
)
const (
errorTypeHeader = "X-Amzn-Errortype"
errorMessageHeader = "X-Amzn-Errormessage"
)
// UnmarshalTypedError provides unmarshaling errors API response errors
// for both typed and untyped errors.
type UnmarshalTypedError struct {
exceptions map[string]func(protocol.ResponseMetadata) error
}
// NewUnmarshalTypedError returns an UnmarshalTypedError initialized for the
// set of exception names to the error unmarshalers
func NewUnmarshalTypedError(exceptions map[string]func(protocol.ResponseMetadata) error) *UnmarshalTypedError {
return &UnmarshalTypedError{
exceptions: exceptions,
}
}
// UnmarshalError attempts to unmarshal the HTTP response error as a known
// error type. If unable to unmarshal the error type, the generic SDK error
// type will be used.
func (u *UnmarshalTypedError) UnmarshalError(
resp *http.Response,
respMeta protocol.ResponseMetadata,
) (error, error) {
code := resp.Header.Get(errorTypeHeader)
msg := resp.Header.Get(errorMessageHeader)
body := resp.Body
if len(code) == 0 {
// If unable to get code from HTTP headers have to parse JSON message
// to determine what kind of exception this will be.
var buf bytes.Buffer
var jsonErr jsonErrorResponse
teeReader := io.TeeReader(resp.Body, &buf)
err := jsonutil.UnmarshalJSONError(&jsonErr, teeReader)
if err != nil {
return nil, err
}
body = ioutil.NopCloser(&buf)
code = jsonErr.Code
msg = jsonErr.Message
}
// If code has colon separators remove them so can compare against modeled
// exception names.
code = strings.SplitN(code, ":", 2)[0]
if fn, ok := u.exceptions[code]; ok {
// If exception code is know, use associated constructor to get a value
// for the exception that the JSON body can be unmarshaled into.
v := fn(respMeta)
if err := jsonutil.UnmarshalJSONCaseInsensitive(v, body); err != nil {
return nil, err
}
if err := rest.UnmarshalResponse(resp, v, true); err != nil {
return nil, err
}
return v, nil
}
// fallback to unmodeled generic exceptions
return awserr.NewRequestFailure(
awserr.New(code, msg, nil),
respMeta.StatusCode,
respMeta.RequestID,
), nil
}
// UnmarshalErrorHandler is a named request handler for unmarshaling restjson
// protocol request errors
var UnmarshalErrorHandler = request.NamedHandler{
Name: "awssdk.restjson.UnmarshalError",
Fn: UnmarshalError,
}
// UnmarshalError unmarshals a response error for the REST JSON protocol.
func UnmarshalError(r *request.Request) {
defer r.HTTPResponse.Body.Close()
var jsonErr jsonErrorResponse
err := jsonutil.UnmarshalJSONError(&jsonErr, r.HTTPResponse.Body)
if err != nil {
r.Error = awserr.NewRequestFailure(
awserr.New(request.ErrCodeSerialization,
"failed to unmarshal response error", err),
r.HTTPResponse.StatusCode,
r.RequestID,
)
return
}
code := r.HTTPResponse.Header.Get(errorTypeHeader)
if code == "" {
code = jsonErr.Code
}
msg := r.HTTPResponse.Header.Get(errorMessageHeader)
if msg == "" {
msg = jsonErr.Message
}
code = strings.SplitN(code, ":", 2)[0]
r.Error = awserr.NewRequestFailure(
awserr.New(code, jsonErr.Message, nil),
r.HTTPResponse.StatusCode,
r.RequestID,
)
}
type jsonErrorResponse struct {
Code string `json:"code"`
Message string `json:"message"`
}

View file

@ -2,8 +2,8 @@
// requests and responses.
package restxml
//go:generate go run -tags codegen ../../../models/protocol_tests/generate.go ../../../models/protocol_tests/input/rest-xml.json build_test.go
//go:generate go run -tags codegen ../../../models/protocol_tests/generate.go ../../../models/protocol_tests/output/rest-xml.json unmarshal_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/input/rest-xml.json build_test.go
//go:generate go run -tags codegen ../../../private/model/cli/gen-protocol-tests ../../../models/protocol_tests/output/rest-xml.json unmarshal_test.go
import (
"bytes"

View file

@ -1,6 +1,8 @@
package protocol
import (
"bytes"
"fmt"
"math"
"strconv"
"time"
@ -19,16 +21,19 @@ const (
// Output time is intended to not contain decimals
const (
// RFC 7231#section-7.1.1.1 timetamp format. e.g Tue, 29 Apr 2014 18:30:38 GMT
RFC822TimeFormat = "Mon, 2 Jan 2006 15:04:05 GMT"
RFC822TimeFormat = "Mon, 2 Jan 2006 15:04:05 GMT"
rfc822TimeFormatSingleDigitDay = "Mon, _2 Jan 2006 15:04:05 GMT"
rfc822TimeFormatSingleDigitDayTwoDigitYear = "Mon, _2 Jan 06 15:04:05 GMT"
// This format is used for output time without seconds precision
RFC822OutputTimeFormat = "Mon, 02 Jan 2006 15:04:05 GMT"
// RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z
ISO8601TimeFormat = "2006-01-02T15:04:05.999999999Z"
ISO8601TimeFormat = "2006-01-02T15:04:05.999999999Z"
iso8601TimeFormatNoZ = "2006-01-02T15:04:05.999999999"
// This format is used for output time without seconds precision
ISO8601OutputTimeFormat = "2006-01-02T15:04:05Z"
// This format is used for output time with fractional second precision up to milliseconds
ISO8601OutputTimeFormat = "2006-01-02T15:04:05.999999999Z"
)
// IsKnownTimestampFormat returns if the timestamp format name
@ -48,7 +53,7 @@ func IsKnownTimestampFormat(name string) bool {
// FormatTime returns a string value of the time.
func FormatTime(name string, t time.Time) string {
t = t.UTC()
t = t.UTC().Truncate(time.Millisecond)
switch name {
case RFC822TimeFormatName:
@ -56,7 +61,8 @@ func FormatTime(name string, t time.Time) string {
case ISO8601TimeFormatName:
return t.Format(ISO8601OutputTimeFormat)
case UnixTimeFormatName:
return strconv.FormatInt(t.Unix(), 10)
ms := t.UnixNano() / int64(time.Millisecond)
return strconv.FormatFloat(float64(ms)/1e3, 'f', -1, 64)
default:
panic("unknown timestamp format name, " + name)
}
@ -66,10 +72,21 @@ func FormatTime(name string, t time.Time) string {
// the time if it was able to be parsed, and fails otherwise.
func ParseTime(formatName, value string) (time.Time, error) {
switch formatName {
case RFC822TimeFormatName:
return time.Parse(RFC822TimeFormat, value)
case ISO8601TimeFormatName:
return time.Parse(ISO8601TimeFormat, value)
case RFC822TimeFormatName: // Smithy HTTPDate format
return tryParse(value,
RFC822TimeFormat,
rfc822TimeFormatSingleDigitDay,
rfc822TimeFormatSingleDigitDayTwoDigitYear,
time.RFC850,
time.ANSIC,
)
case ISO8601TimeFormatName: // Smithy DateTime format
return tryParse(value,
ISO8601TimeFormat,
iso8601TimeFormatNoZ,
time.RFC3339Nano,
time.RFC3339,
)
case UnixTimeFormatName:
v, err := strconv.ParseFloat(value, 64)
_, dec := math.Modf(v)
@ -82,3 +99,36 @@ func ParseTime(formatName, value string) (time.Time, error) {
panic("unknown timestamp format name, " + formatName)
}
}
func tryParse(v string, formats ...string) (time.Time, error) {
var errs parseErrors
for _, f := range formats {
t, err := time.Parse(f, v)
if err != nil {
errs = append(errs, parseError{
Format: f,
Err: err,
})
continue
}
return t, nil
}
return time.Time{}, fmt.Errorf("unable to parse time string, %v", errs)
}
type parseErrors []parseError
func (es parseErrors) Error() string {
var s bytes.Buffer
for _, e := range es {
fmt.Fprintf(&s, "\n * %q: %v", e.Format, e.Err)
}
return "parse errors:" + s.String()
}
type parseError struct {
Format string
Err error
}

View file

@ -8,6 +8,7 @@ import (
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/private/protocol"
@ -60,6 +61,14 @@ func (b *xmlBuilder) buildValue(value reflect.Value, current *XMLNode, tag refle
return nil
}
xml := tag.Get("xml")
if len(xml) != 0 {
name := strings.SplitAfterN(xml, ",", 2)[0]
if name == "-" {
return nil
}
}
t := tag.Get("type")
if t == "" {
switch value.Kind() {
@ -299,6 +308,8 @@ func (b *xmlBuilder) buildScalar(value reflect.Value, current *XMLNode, tag refl
if tag.Get("xmlAttribute") != "" { // put into current node's attribute list
attr := xml.Attr{Name: xname, Value: str}
current.Attr = append(current.Attr, attr)
} else if len(xname.Local) == 0 {
current.Text = str
} else { // regular text node
current.AddChild(&XMLNode{Name: xname, Text: str})
}

View file

@ -64,6 +64,14 @@ func UnmarshalXML(v interface{}, d *xml.Decoder, wrapper string) error {
// parse deserializes any value from the XMLNode. The type tag is used to infer the type, or reflect
// will be used to determine the type from r.
func parse(r reflect.Value, node *XMLNode, tag reflect.StructTag) error {
xml := tag.Get("xml")
if len(xml) != 0 {
name := strings.SplitAfterN(xml, ",", 2)[0]
if name == "-" {
return nil
}
}
rtype := r.Type()
if rtype.Kind() == reflect.Ptr {
rtype = rtype.Elem() // check kind of actual element type

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