Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions packages/core/src/auth/deprecated/loginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,11 @@ import { isAutomation } from '../../shared/vscode/env'
import { Credentials } from '@aws-sdk/types'
import { ToolkitError } from '../../shared/errors'
import * as localizedText from '../../shared/localizedText'
import { DefaultStsClient, type GetCallerIdentityResponse } from '../../shared/clients/stsClient'
import {
DefaultStsClient,
type GetCallerIdentityResponse,
type GetCallerIdentityResponseWithHeaders,
} from '../../shared/clients/stsClient'
import { findAsync } from '../../shared/utilities/collectionUtils'
import { telemetry } from '../../shared/telemetry/telemetry'
import { withTelemetryContext } from '../../shared/telemetry/util'
Expand Down Expand Up @@ -135,9 +139,11 @@ export class LoginManager {
return accountId
}

private async detectExternalConnection(callerIdentity: GetCallerIdentityResponse): Promise<void> {
// @ts-ignore
const headers = callerIdentity.$response?.httpResponse?.headers
private async detectExternalConnection(
callerIdentity: GetCallerIdentityResponse | GetCallerIdentityResponseWithHeaders
): Promise<void> {
// SDK v3: Headers are captured via middleware and attached as $httpHeaders
const headers = (callerIdentity as GetCallerIdentityResponseWithHeaders).$httpHeaders
if (headers !== undefined && localStackConnectionHeader in headers) {
await globals.globalState.update('aws.toolkit.externalConnection', localStackConnectionString)
telemetry.auth_localstackEndpoint.emit({ source: 'validateCredentials', result: 'Succeeded' })
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lambda/remoteDebugging/ldkClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { DefaultLambdaClient } from '../../shared/clients/lambdaClient'
import { LocalProxy } from './localProxy'
import globals from '../../shared/extensionGlobals'
import { getLogger } from '../../shared/logger/logger'
import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgent } from './utils'
import { getIoTSTClientWithAgent, getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from './utils'
import { ToolkitError } from '../../shared/errors'
import * as nls from 'vscode-nls'

Expand Down Expand Up @@ -99,7 +99,7 @@ export class LdkClient {
*/
private getLambdaClient(region: string): DefaultLambdaClient {
if (!this.lambdaClientCache.has(region)) {
this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgent()))
this.lambdaClientCache.set(region, getLambdaClientWithAgent(region, getLambdaDebugUserAgentPairs()))
}
return this.lambdaClientCache.get(region)!
}
Expand Down
38 changes: 24 additions & 14 deletions packages/core/src/lambda/remoteDebugging/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,46 @@

import { IoTSecureTunnelingClient } from '@aws-sdk/client-iotsecuretunneling'
import { DefaultLambdaClient } from '../../shared/clients/lambdaClient'
import { getUserAgent } from '../../shared/telemetry/util'
import { getUserAgentPairs, userAgentPairsToString } from '../../shared/telemetry/util'
import globals from '../../shared/extensionGlobals'
import type { UserAgent } from '@aws-sdk/types'

const customUserAgentBase = 'LAMBDA-DEBUG/1.0.0'
const customUserAgentBase: [string, string] = ['LAMBDA-DEBUG', '1.0.0']

export function getLambdaClientWithAgent(region: string, customUserAgent?: string): DefaultLambdaClient {
export function getLambdaClientWithAgent(region: string, customUserAgent?: UserAgent): DefaultLambdaClient {
if (!customUserAgent) {
customUserAgent = getLambdaUserAgent()
customUserAgent = getLambdaUserAgentPairs()
}
return new DefaultLambdaClient(region, customUserAgent)
}

// Example user agent:
// LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111
export function getLambdaDebugUserAgent(): string {
return `${customUserAgentBase} ${getLambdaUserAgent()}`
/**
* Returns properly formatted UserAgent pairs for AWS SDK v3
*/
export function getLambdaDebugUserAgentPairs(): UserAgent {
return [customUserAgentBase, ...getUserAgentPairs({ includePlatform: true, includeClientId: true })]
}

// Example user agent:
// AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.102.2 ClientId/11111111-1111-1111-1111-111111111111
export function getLambdaUserAgent(): string {
return `${getUserAgent({ includePlatform: true, includeClientId: true })}`
/**
* Returns properly formatted UserAgent pairs for AWS SDK v3
*/
export function getLambdaUserAgentPairs(): UserAgent {
return getUserAgentPairs({ includePlatform: true, includeClientId: true })
}

/**
* Returns user agent string for Lambda debugging in traditional format.
* Example: "LAMBDA-DEBUG/1.0.0 AWS-Toolkit-For-VSCode/testPluginVersion Visual-Studio-Code/1.105.1 ClientId/11111111-1111-1111-1111-111111111111"
*/
export function getLambdaDebugUserAgent(): string {
return userAgentPairsToString(getLambdaDebugUserAgentPairs())
}

export function getIoTSTClientWithAgent(region: string): IoTSecureTunnelingClient {
const customUserAgent = `${customUserAgentBase} ${getUserAgent({ includePlatform: true, includeClientId: true })}`
return globals.sdkClientBuilderV3.createAwsService({
serviceClient: IoTSecureTunnelingClient,
clientOptions: {
userAgent: [[customUserAgent]],
customUserAgent: [customUserAgentBase],
region,
},
userAgent: false,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/lambda/vue/remoteInvoke/invokeLambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { getLambdaHandlerFile } from '../../../awsService/appBuilder/utils'
import { runUploadDirectory } from '../../commands/uploadLambda'
import fs from '../../../shared/fs/fs'
import { showConfirmationMessage, showMessage } from '../../../shared/utilities/messages'
import { getLambdaClientWithAgent, getLambdaDebugUserAgent } from '../../remoteDebugging/utils'
import { getLambdaClientWithAgent, getLambdaDebugUserAgentPairs } from '../../remoteDebugging/utils'
import { isLocalStackConnection } from '../../../auth/utils'
import { getRemoteDebugLayer } from '../../remoteDebugging/remoteLambdaDebugger'

Expand Down Expand Up @@ -906,7 +906,7 @@ export async function invokeRemoteLambda(
const resource: LambdaFunctionNode = params.functionNode
const source: string = params.source || 'AwsExplorerRemoteInvoke'
const client = getLambdaClientWithAgent(resource.regionCode)
const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgent())
const clientDebug = getLambdaClientWithAgent(resource.regionCode, getLambdaDebugUserAgentPairs())

const Panel = VueWebview.compilePanel(RemoteInvokeWebview)

Expand Down
30 changes: 25 additions & 5 deletions packages/core/src/shared/awsClientBuilderV3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
TokenIdentity,
TokenIdentityProvider,
} from '@smithy/types'
import { getUserAgent } from './telemetry/util'
import { getUserAgentPairs } from './telemetry/util'
import { DevSettings } from './settings'
import {
BuildHandler,
Expand All @@ -37,7 +37,6 @@ import { HttpResponse, HttpRequest } from '@aws-sdk/protocol-http'
import { ConfiguredRetryStrategy } from '@smithy/util-retry'
import { telemetry } from './telemetry/telemetry'
import { getRequestId, getTelemetryReason, getTelemetryReasonDesc, getTelemetryResult } from './errors'
import { extensionVersion } from './vscode/env'
import { getLogger } from './logger/logger'
import { partialClone } from './utilities/collectionUtils'
import { selectFrom } from './utilities/tsUtils'
Expand Down Expand Up @@ -72,7 +71,7 @@ export interface AwsCommand<InputType extends object, OutputType extends object>
export interface AwsClientOptions {
credentials: AwsCredentialIdentityProvider
region: string | Provider<string>
userAgent: UserAgent
customUserAgent: UserAgent
requestHandler: {
metadata?: RequestHandlerMetadata
handle: (req: any, options?: any) => Promise<RequestHandlerOutput<any>>
Expand Down Expand Up @@ -155,8 +154,8 @@ export class AWSClientBuilderV3 {
opt.region = serviceOptions.region
}

if (!opt.userAgent && userAgent) {
opt.userAgent = [[getUserAgent({ includePlatform: true, includeClientId: true }), extensionVersion]]
if (!opt.customUserAgent && userAgent) {
opt.customUserAgent = getUserAgentPairs({ includePlatform: true, includeClientId: true })
}

if (!opt.retryStrategy) {
Expand Down Expand Up @@ -196,6 +195,7 @@ export class AWSClientBuilderV3 {
}
const service = new serviceOptions.serviceClient(opt)
service.middlewareStack.add(telemetryMiddleware, { step: 'deserialize' })
service.middlewareStack.add(captureHeadersMiddleware, { step: 'deserialize' })
service.middlewareStack.add(loggingMiddleware, { step: 'finalizeRequest' })
service.middlewareStack.add(getEndpointMiddleware(serviceOptions.settings), { step: 'build' })

Expand Down Expand Up @@ -254,6 +254,26 @@ function getEndpointMiddleware(settings: DevSettings = DevSettings.instance): Bu
const keepAliveMiddleware: BuildMiddleware<any, any> = (next: BuildHandler<any, any>) => async (args: any) =>
addKeepAliveHeader(next, args)

/**
* Middleware that captures HTTP response headers and attaches them to the output object.
* This makes headers accessible via `response.$httpHeaders` for all AWS SDK v3 operations.
* Useful for detecting custom headers from services like LocalStack.
*/
const captureHeadersMiddleware: DeserializeMiddleware<any, any> =
(next: DeserializeHandler<any, any>) => async (args: any) => {
const result = await next(args)

// Extract headers from HTTP response and attach to output for easy access
if (HttpResponse.isInstance(result.response)) {
const headers = result.response.headers
if (headers && result.output) {
result.output.$httpHeaders = headers
}
}

return result
}

export async function emitOnRequest(next: DeserializeHandler<any, any>, context: HandlerExecutionContext, args: any) {
if (!HttpResponse.isInstance(args.request)) {
return next(args)
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/shared/clients/lambdaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { CancellationError } from '../utilities/timeoutUtils'
import { fromSSO } from '@aws-sdk/credential-provider-sso'
import { getIAMConnection } from '../../auth/utils'
import { NodeHttpHandler } from '@smithy/node-http-handler'
import type { UserAgent } from '@aws-sdk/types'

export type LambdaClient = ClassToInterfaceType<DefaultLambdaClient>

Expand All @@ -49,7 +50,7 @@ export class DefaultLambdaClient {

public constructor(
public readonly regionCode: string,
public readonly userAgent: string | undefined = undefined
public readonly userAgent: UserAgent | undefined = undefined
) {
this.defaultTimeoutInMs = 5 * 60 * 1000 // 5 minutes (SDK default is 2 minutes)
}
Expand Down Expand Up @@ -322,7 +323,7 @@ export class DefaultLambdaClient {
serviceClient: LambdaSdkClient,
userAgent: !this.userAgent,
clientOptions: {
userAgent: this.userAgent ? [[this.userAgent]] : undefined,
customUserAgent: this.userAgent,
region: this.regionCode,
requestHandler: new NodeHttpHandler({
requestTimeout: this.defaultTimeoutInMs,
Expand Down
8 changes: 7 additions & 1 deletion packages/core/src/shared/clients/stsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { Credentials } from '@aws-sdk/types'
import globals from '../extensionGlobals'
import { ClassToInterfaceType } from '../utilities/tsUtils'

// Extended response type that includes captured HTTP headers (added by global middleware)
export interface GetCallerIdentityResponseWithHeaders extends GetCallerIdentityResponse {
$httpHeaders?: Record<string, string>
}

export type { GetCallerIdentityResponse }
export type StsClient = ClassToInterfaceType<DefaultStsClient>

Expand All @@ -35,8 +40,9 @@ export class DefaultStsClient {
return response
}

public async getCallerIdentity(): Promise<GetCallerIdentityResponse> {
public async getCallerIdentity(): Promise<GetCallerIdentityResponseWithHeaders> {
const sdkClient = this.createSdkClient()
// Note: $httpHeaders are added by global middleware in awsClientBuilderV3
const response = await sdkClient.send(new GetCallerIdentityCommand({}))
return response
}
Expand Down
39 changes: 33 additions & 6 deletions packages/core/src/shared/telemetry/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { telemetry } from './telemetry'
import { v5 as uuidV5 } from 'uuid'
import { ToolkitError } from '../errors'
import { GlobalState } from '../globalState'
import type { UserAgent } from '@aws-sdk/types'

const legacySettingsTelemetryValueDisable = 'Disable'
const legacySettingsTelemetryValueEnable = 'Enable'
Expand Down Expand Up @@ -240,20 +241,46 @@ export function getUserAgent(
opt?: { includePlatform?: boolean; includeClientId?: boolean },
globalState = globals.globalState
): string {
const pairs = isAmazonQ()
? [`AmazonQ-For-VSCode/${extensionVersion}`]
: [`AWS-Toolkit-For-VSCode/${extensionVersion}`]
return userAgentPairsToString(getUserAgentPairs(opt, globalState))
}

/**
* Returns a UserAgent array (AWS SDK v3 format) with proper [name, version] pairs.
*
* Omits the platform and `ClientId` pairs by default.
*
* @returns Array of [name, version] tuples for AWS SDK v3's customUserAgent option
*/
export function getUserAgentPairs(
opt?: { includePlatform?: boolean; includeClientId?: boolean },
globalState = globals.globalState
): UserAgent {
const pairs: UserAgent = isAmazonQ()
? [['AmazonQ-For-VSCode', extensionVersion]]
: [['AWS-Toolkit-For-VSCode', extensionVersion]]

if (opt?.includePlatform) {
pairs.push(platformPair())
const platform = platformPair()
const [name, version] = platform.split('/')
if (name && version) {
pairs.push([name, version])
}
}

if (opt?.includeClientId) {
const clientId = getClientId(globalState)
pairs.push(`ClientId/${clientId}`)
pairs.push(['ClientId', clientId])
}

return pairs.join(' ')
return pairs
}

/**
* Converts UserAgent array format to traditional user agent string format.
* Example: [['LAMBDA-DEBUG', '1.0.0'], ['AWS-Toolkit', '2.0']] => "LAMBDA-DEBUG/1.0.0 AWS-Toolkit/2.0"
*/
export function userAgentPairsToString(pairs: UserAgent): string {
return pairs.map(([name, version]) => `${name}/${version}`).join(' ')
}

/**
Expand Down
42 changes: 30 additions & 12 deletions packages/core/src/test/lambda/remoteDebugging/ldkClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,9 +434,13 @@ describe('LdkClient', () => {
existingStub.restore()
}

// Stub getUserAgent at the telemetryUtil level to return a known value
const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent')
getUserAgentStub.returns('test-user-agent')
// Stub getUserAgentPairs at the telemetryUtil level to return known pairs
const getUserAgentPairsStub = sandbox.stub(telemetryUtil, 'getUserAgentPairs')
getUserAgentPairsStub.returns([
['AWS-Toolkit-For-VSCode', 'testVersion'],
['Visual-Studio-Code', '1.0.0'],
['ClientId', 'test-client-id'],
])

// Stub the sdkClientBuilderV3 to capture the client options
let capturedClientOptions: any
Expand Down Expand Up @@ -464,9 +468,14 @@ describe('LdkClient', () => {
assert(createAwsServiceStub.called, 'Should call createAwsService')
assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region')
assert.deepStrictEqual(
capturedClientOptions.clientOptions.userAgent,
[['LAMBDA-DEBUG/1.0.0 test-user-agent']],
'Should include correct user-agent with LAMBDA-DEBUG prefix in Lambda API calls'
capturedClientOptions.clientOptions.customUserAgent,
[
['LAMBDA-DEBUG', '1.0.0'],
['AWS-Toolkit-For-VSCode', 'testVersion'],
['Visual-Studio-Code', '1.0.0'],
['ClientId', 'test-client-id'],
],
'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix in Lambda API calls'
)
})

Expand All @@ -479,9 +488,13 @@ describe('LdkClient', () => {
existingStub.restore()
}

// Stub getUserAgent to return a known value
const getUserAgentStub = sandbox.stub(telemetryUtil, 'getUserAgent')
getUserAgentStub.returns('test-user-agent')
// Stub getUserAgentPairs to return known pairs
const getUserAgentPairsStub = sandbox.stub(telemetryUtil, 'getUserAgentPairs')
getUserAgentPairsStub.returns([
['AWS-Toolkit-For-VSCode', 'testVersion'],
['Visual-Studio-Code', '1.0.0'],
['ClientId', 'test-client-id'],
])

// Stub the sdkClientBuilderV3 to capture the client options
let capturedClientOptions: any
Expand All @@ -503,9 +516,14 @@ describe('LdkClient', () => {
assert(createAwsServiceStub.calledOnce, 'Should call createAwsService once')
assert.strictEqual(capturedClientOptions.clientOptions.region, 'us-east-1', 'Should use correct region')
assert.deepStrictEqual(
capturedClientOptions.clientOptions.userAgent,
[['LAMBDA-DEBUG/1.0.0 test-user-agent']],
'Should include correct user-agent with LAMBDA-DEBUG prefix'
capturedClientOptions.clientOptions.customUserAgent,
[
['LAMBDA-DEBUG', '1.0.0'],
['AWS-Toolkit-For-VSCode', 'testVersion'],
['Visual-Studio-Code', '1.0.0'],
['ClientId', 'test-client-id'],
],
'Should include correct customUserAgent pairs with LAMBDA-DEBUG prefix'
)
})
})
Expand Down
Loading
Loading