1package middleware
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"runtime"
8	"strings"
9
10	"github.com/aws/aws-sdk-go-v2/aws"
11	"github.com/aws/smithy-go/middleware"
12	smithyhttp "github.com/aws/smithy-go/transport/http"
13)
14
15var languageVersion = strings.TrimPrefix(runtime.Version(), "go")
16
17// SDKAgentKeyType is the metadata type to add to the SDK agent string
18type SDKAgentKeyType int
19
20// The set of valid SDKAgentKeyType constants. If an unknown value is assigned for SDKAgentKeyType it will
21// be mapped to AdditionalMetadata.
22const (
23	_ SDKAgentKeyType = iota
24	APIMetadata
25	OperatingSystemMetadata
26	LanguageMetadata
27	EnvironmentMetadata
28	FeatureMetadata
29	ConfigMetadata
30	FrameworkMetadata
31	AdditionalMetadata
32	ApplicationIdentifier
33)
34
35func (k SDKAgentKeyType) string() string {
36	switch k {
37	case APIMetadata:
38		return "api"
39	case OperatingSystemMetadata:
40		return "os"
41	case LanguageMetadata:
42		return "lang"
43	case EnvironmentMetadata:
44		return "exec-env"
45	case FeatureMetadata:
46		return "ft"
47	case ConfigMetadata:
48		return "cfg"
49	case FrameworkMetadata:
50		return "lib"
51	case ApplicationIdentifier:
52		return "app"
53	case AdditionalMetadata:
54		fallthrough
55	default:
56		return "md"
57	}
58}
59
60const execEnvVar = `AWS_EXECUTION_ENV`
61
62// requestUserAgent is a build middleware that set the User-Agent for the request.
63type requestUserAgent struct {
64	sdkAgent, userAgent *smithyhttp.UserAgentBuilder
65}
66
67// newRequestUserAgent returns a new requestUserAgent which will set the User-Agent and X-Amz-User-Agent for the
68// request.
69//
70// User-Agent example:
71//   aws-sdk-go-v2/1.2.3
72//
73// X-Amz-User-Agent example:
74//   aws-sdk-go-v2/1.2.3 md/GOOS/linux md/GOARCH/amd64 lang/go/1.15
75func newRequestUserAgent() *requestUserAgent {
76	userAgent, sdkAgent := smithyhttp.NewUserAgentBuilder(), smithyhttp.NewUserAgentBuilder()
77	addProductName(userAgent)
78	addProductName(sdkAgent)
79
80	r := &requestUserAgent{
81		sdkAgent:  sdkAgent,
82		userAgent: userAgent,
83	}
84
85	addSDKMetadata(r)
86
87	return r
88}
89
90func getNormalizedOSName() (os string) {
91	switch runtime.GOOS {
92	case "android":
93		os = "android"
94	case "linux":
95		os = "linux"
96	case "windows":
97		os = "windows"
98	case "darwin":
99		// Due to Apple M1 we can't distinguish between macOS and iOS when GOOS/GOARCH is darwin/amd64
100		// For now declare this as "other" until we have a better detection mechanism.
101		fallthrough
102	default:
103		os = "other"
104	}
105	return os
106}
107
108func addSDKMetadata(r *requestUserAgent) {
109	r.AddSDKAgentKey(OperatingSystemMetadata, getNormalizedOSName())
110	r.AddSDKAgentKeyValue(LanguageMetadata, "go", languageVersion)
111	r.AddSDKAgentKeyValue(AdditionalMetadata, "GOOS", runtime.GOOS)
112	r.AddSDKAgentKeyValue(AdditionalMetadata, "GOARCH", runtime.GOARCH)
113	if ev := os.Getenv(execEnvVar); len(ev) > 0 {
114		r.AddSDKAgentKey(EnvironmentMetadata, ev)
115	}
116}
117
118func addProductName(builder *smithyhttp.UserAgentBuilder) {
119	builder.AddKeyValue(aws.SDKName, aws.SDKVersion)
120}
121
122// AddUserAgentKey retrieves a requestUserAgent from the provided stack, or initializes one.
123func AddUserAgentKey(key string) func(*middleware.Stack) error {
124	return func(stack *middleware.Stack) error {
125		requestUserAgent, err := getOrAddRequestUserAgent(stack)
126		if err != nil {
127			return err
128		}
129		requestUserAgent.AddUserAgentKey(key)
130		return nil
131	}
132}
133
134// AddUserAgentKeyValue retrieves a requestUserAgent from the provided stack, or initializes one.
135func AddUserAgentKeyValue(key, value string) func(*middleware.Stack) error {
136	return func(stack *middleware.Stack) error {
137		requestUserAgent, err := getOrAddRequestUserAgent(stack)
138		if err != nil {
139			return err
140		}
141		requestUserAgent.AddUserAgentKeyValue(key, value)
142		return nil
143	}
144}
145
146// AddSDKAgentKey retrieves a requestUserAgent from the provided stack, or initializes one.
147func AddSDKAgentKey(keyType SDKAgentKeyType, key string) func(*middleware.Stack) error {
148	return func(stack *middleware.Stack) error {
149		requestUserAgent, err := getOrAddRequestUserAgent(stack)
150		if err != nil {
151			return err
152		}
153		requestUserAgent.AddSDKAgentKey(keyType, key)
154		return nil
155	}
156}
157
158// AddSDKAgentKeyValue retrieves a requestUserAgent from the provided stack, or initializes one.
159func AddSDKAgentKeyValue(keyType SDKAgentKeyType, key, value string) func(*middleware.Stack) error {
160	return func(stack *middleware.Stack) error {
161		requestUserAgent, err := getOrAddRequestUserAgent(stack)
162		if err != nil {
163			return err
164		}
165		requestUserAgent.AddSDKAgentKeyValue(keyType, key, value)
166		return nil
167	}
168}
169
170// AddRequestUserAgentMiddleware registers a requestUserAgent middleware on the stack if not present.
171func AddRequestUserAgentMiddleware(stack *middleware.Stack) error {
172	_, err := getOrAddRequestUserAgent(stack)
173	return err
174}
175
176func getOrAddRequestUserAgent(stack *middleware.Stack) (*requestUserAgent, error) {
177	id := (*requestUserAgent)(nil).ID()
178	bm, ok := stack.Build.Get(id)
179	if !ok {
180		bm = newRequestUserAgent()
181		err := stack.Build.Add(bm, middleware.After)
182		if err != nil {
183			return nil, err
184		}
185	}
186
187	requestUserAgent, ok := bm.(*requestUserAgent)
188	if !ok {
189		return nil, fmt.Errorf("%T for %s middleware did not match expected type", bm, id)
190	}
191
192	return requestUserAgent, nil
193}
194
195// AddUserAgentKey adds the component identified by name to the User-Agent string.
196func (u *requestUserAgent) AddUserAgentKey(key string) {
197	u.userAgent.AddKey(key)
198}
199
200// AddUserAgentKeyValue adds the key identified by the given name and value to the User-Agent string.
201func (u *requestUserAgent) AddUserAgentKeyValue(key, value string) {
202	u.userAgent.AddKeyValue(key, value)
203}
204
205// AddUserAgentKey adds the component identified by name to the User-Agent string.
206func (u *requestUserAgent) AddSDKAgentKey(keyType SDKAgentKeyType, key string) {
207	u.sdkAgent.AddKey(keyType.string() + "/" + key)
208}
209
210// AddUserAgentKeyValue adds the key identified by the given name and value to the User-Agent string.
211func (u *requestUserAgent) AddSDKAgentKeyValue(keyType SDKAgentKeyType, key, value string) {
212	u.sdkAgent.AddKeyValue(keyType.string()+"/"+key, value)
213}
214
215// ID the name of the middleware.
216func (u *requestUserAgent) ID() string {
217	return "UserAgent"
218}
219
220// HandleBuild adds or appends the constructed user agent to the request.
221func (u *requestUserAgent) HandleBuild(ctx context.Context, in middleware.BuildInput, next middleware.BuildHandler) (
222	out middleware.BuildOutput, metadata middleware.Metadata, err error,
223) {
224	switch req := in.Request.(type) {
225	case *smithyhttp.Request:
226		u.addHTTPUserAgent(req)
227		u.addHTTPSDKAgent(req)
228	default:
229		return out, metadata, fmt.Errorf("unknown transport type %T", in)
230	}
231
232	return next.HandleBuild(ctx, in)
233}
234
235func (u *requestUserAgent) addHTTPUserAgent(request *smithyhttp.Request) {
236	const userAgent = "User-Agent"
237	updateHTTPHeader(request, userAgent, u.userAgent.Build())
238}
239
240func (u *requestUserAgent) addHTTPSDKAgent(request *smithyhttp.Request) {
241	const sdkAgent = "X-Amz-User-Agent"
242	updateHTTPHeader(request, sdkAgent, u.sdkAgent.Build())
243}
244
245func updateHTTPHeader(request *smithyhttp.Request, header string, value string) {
246	var current string
247	if v := request.Header[header]; len(v) > 0 {
248		current = v[0]
249	}
250	if len(current) > 0 {
251		current = value + " " + current
252	} else {
253		current = value
254	}
255	request.Header[header] = append(request.Header[header][:0], current)
256}
257