From d4928a472b3b1d2ed4e2b65586aeec6f223adeba Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Mon, 5 Jun 2023 17:11:59 -0700 Subject: [PATCH 1/4] fix(api): GraphQL subscription with custom domain formats URI correctly --- .../src/decorators/web_socket_auth_utils.dart | 21 +++++++++++++++++-- packages/api/amplify_api_dart/test/util.dart | 9 ++++++++ .../web_socket_auth_utils_test.dart | 12 +++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart index 52d83d1b5a..e84c5b5375 100644 --- a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart +++ b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart @@ -11,6 +11,15 @@ import 'package:amplify_api_dart/src/graphql/web_socket/types/web_socket_types.d import 'package:amplify_core/amplify_core.dart'; import 'package:meta/meta.dart'; +// https:// +// followed by 1 or more lowercase letters/numbers +// followed by .appsync-api. +// followed by 1 or more lowercase letters/numbers/hyphens (region) +// followed by .amazonaws.com/graphql +// Adapted from https://github.com/aws-amplify/amplify-android/blob/6e441f60ae5525644340edff13f3287c3c8960f8/aws-api/src/main/java/com/amplifyframework/api/aws/DomainType.java#L36 +const _appSyncEndpointPattern = + r'^https://[a-z0-9]+\.appsync-api\.[a-z0-9-]+\.amazonaws.com/graphql'; + // Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html. const _requiredHeaders = { AWSHeaders.accept: 'application/json, text/javascript', @@ -36,10 +45,18 @@ Future generateConnectionUri( ); final encodedAuthHeaders = base64.encode(json.encode(authorizationHeaders).codeUnits); - final endpointUri = Uri.parse( + var endpointUri = Uri.parse( config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'), ); - return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql').replace( + var path = 'graphql'; + // Check for custom domain and format URL as documented on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. + final appSyncDomainRegExp = RegExp(_appSyncEndpointPattern); + if (!appSyncDomainRegExp.hasMatch(config.endpoint)) { + endpointUri = Uri.parse(config.endpoint); + path = 'graphql/realtime'; + } + + return Uri(scheme: 'wss', host: endpointUri.host, path: path).replace( queryParameters: { 'header': encodedAuthHeaders, 'payload': base64.encode(utf8.encode(json.encode(_emptyBody))), diff --git a/packages/api/amplify_api_dart/test/util.dart b/packages/api/amplify_api_dart/test/util.dart index c2e57b7951..c97f8404d2 100644 --- a/packages/api/amplify_api_dart/test/util.dart +++ b/packages/api/amplify_api_dart/test/util.dart @@ -75,9 +75,18 @@ const testApiKeyConfig = AWSApiConfig( authorizationType: APIAuthorizationType.apiKey, apiKey: 'abc-123', ); +const testApiKeyConfigCustomDomain = AWSApiConfig( + endpointType: EndpointType.graphQL, + endpoint: 'https://foo.bar.aws.dev/graphql ', + region: 'us-east-1', + authorizationType: APIAuthorizationType.apiKey, + apiKey: 'abc-123', +); const expectedApiKeyWebSocketConnectionUrl = 'wss://abc123.appsync-realtime-api.us-east-1.amazonaws.com/graphql?header=eyJBY2NlcHQiOiJhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L2phdmFzY3JpcHQiLCJDb250ZW50LUVuY29kaW5nIjoiYW16LTEuMCIsIkNvbnRlbnQtVHlwZSI6ImFwcGxpY2F0aW9uL2pzb247IGNoYXJzZXQ9dXRmLTgiLCJYLUFwaS1LZXkiOiJhYmMtMTIzIiwiSG9zdCI6ImFiYzEyMy5hcHBzeW5jLWFwaS51cy1lYXN0LTEuYW1hem9uYXdzLmNvbSJ9&payload=e30%3D'; +const expectedApiKeyWebSocketConnectionUrlCustomDomain = + 'wss://foo.bar.aws.dev/graphql/realtime?header=eyJBY2NlcHQiOiJhcHBsaWNhdGlvbi9qc29uLCB0ZXh0L2phdmFzY3JpcHQiLCJDb250ZW50LUVuY29kaW5nIjoiYW16LTEuMCIsIkNvbnRlbnQtVHlwZSI6ImFwcGxpY2F0aW9uL2pzb247IGNoYXJzZXQ9dXRmLTgiLCJYLUFwaS1LZXkiOiJhYmMtMTIzIiwiSG9zdCI6ImZvby5iYXIuYXdzLmRldiJ9&payload=e30%3D'; AmplifyAuthProviderRepository getTestAuthProviderRepo() { final testAuthProviderRepo = AmplifyAuthProviderRepository() diff --git a/packages/api/amplify_api_dart/test/web_socket/web_socket_auth_utils_test.dart b/packages/api/amplify_api_dart/test/web_socket/web_socket_auth_utils_test.dart index a972c40c94..ed9d410082 100644 --- a/packages/api/amplify_api_dart/test/web_socket/web_socket_auth_utils_test.dart +++ b/packages/api/amplify_api_dart/test/web_socket/web_socket_auth_utils_test.dart @@ -55,6 +55,18 @@ void main() { expectedApiKeyWebSocketConnectionUrl, ); }); + + test('should generate authorized connection URI with a custom domain', + () async { + final actualConnectionUri = await generateConnectionUri( + testApiKeyConfigCustomDomain, + authProviderRepo, + ); + expect( + actualConnectionUri.toString(), + expectedApiKeyWebSocketConnectionUrlCustomDomain, + ); + }); }); group('generateSubscriptionRegistrationMessage', () { From e211fb8048ae119190e70589d3d51f39c367e283 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 6 Jun 2023 10:35:38 -0700 Subject: [PATCH 2/4] simplify URI matching --- .../src/decorators/web_socket_auth_utils.dart | 58 +++++++++++-------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart index e84c5b5375..63d78eed6c 100644 --- a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart +++ b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart @@ -11,14 +11,9 @@ import 'package:amplify_api_dart/src/graphql/web_socket/types/web_socket_types.d import 'package:amplify_core/amplify_core.dart'; import 'package:meta/meta.dart'; -// https:// -// followed by 1 or more lowercase letters/numbers -// followed by .appsync-api. -// followed by 1 or more lowercase letters/numbers/hyphens (region) -// followed by .amazonaws.com/graphql -// Adapted from https://github.com/aws-amplify/amplify-android/blob/6e441f60ae5525644340edff13f3287c3c8960f8/aws-api/src/main/java/com/amplifyframework/api/aws/DomainType.java#L36 -const _appSyncEndpointPattern = - r'^https://[a-z0-9]+\.appsync-api\.[a-z0-9-]+\.amazonaws.com/graphql'; +const _appSyncHostPortion = 'appsync-api'; +const _appSyncRealtimeHostPortion = 'appsync-realtime-api'; +const _wssScheme = 'wss'; // Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html. const _requiredHeaders = { @@ -37,6 +32,7 @@ Future generateConnectionUri( AWSApiConfig config, AmplifyAuthProviderRepository authRepo, ) async { + // First, generate auth query parameters. final authorizationHeaders = await _generateAuthorizationHeaders( config, isConnectionInit: true, @@ -45,22 +41,38 @@ Future generateConnectionUri( ); final encodedAuthHeaders = base64.encode(json.encode(authorizationHeaders).codeUnits); - var endpointUri = Uri.parse( - config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'), - ); - var path = 'graphql'; - // Check for custom domain and format URL as documented on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. - final appSyncDomainRegExp = RegExp(_appSyncEndpointPattern); - if (!appSyncDomainRegExp.hasMatch(config.endpoint)) { - endpointUri = Uri.parse(config.endpoint); - path = 'graphql/realtime'; + final authQueryParameters = { + 'header': encodedAuthHeaders, + 'payload': base64.encode(utf8.encode(json.encode(_emptyBody))), + }; + // Conditionally format the URI for a) AppSync domain b) custom domain. In either + // case, add the auth query parameters. + final endpointUriHost = Uri.parse(config.endpoint).host; + if (endpointUriHost.contains(_appSyncHostPortion) && + endpointUriHost.endsWith('amazonaws.com')) { + // AppSync domain that contains "appsync-api" and ends with "amazonaws.com." + // Replace "appsync-api" with "appsync-realtime-api," append "/graphql" and + // add formatted auth query parameters. + final appSyncRealtimeUri = Uri.parse( + config.endpoint + .replaceFirst(_appSyncHostPortion, _appSyncRealtimeHostPortion), + ); + return Uri( + scheme: _wssScheme, + host: appSyncRealtimeUri.host, + path: 'graphql', + ).replace( + queryParameters: authQueryParameters, + ); } - - return Uri(scheme: 'wss', host: endpointUri.host, path: path).replace( - queryParameters: { - 'header': encodedAuthHeaders, - 'payload': base64.encode(utf8.encode(json.encode(_emptyBody))), - }, + // Custom domain (not AppSync); format URL as documented on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. + // Appends "/graphql/realtime" path in addition to auth query parameters. + return Uri( + scheme: _wssScheme, + host: endpointUriHost, + path: 'graphql/realtime', + ).replace( + queryParameters: authQueryParameters, ); } From b1edd87a97d6e21aefdd59bda1cb2c1ebc59e94b Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Tue, 6 Jun 2023 14:41:30 -0700 Subject: [PATCH 3/4] consolidate return code --- .../src/decorators/web_socket_auth_utils.dart | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart index 63d78eed6c..a2a4438a1f 100644 --- a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart +++ b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart @@ -13,7 +13,6 @@ import 'package:meta/meta.dart'; const _appSyncHostPortion = 'appsync-api'; const _appSyncRealtimeHostPortion = 'appsync-realtime-api'; -const _wssScheme = 'wss'; // Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html. const _requiredHeaders = { @@ -45,32 +44,27 @@ Future generateConnectionUri( 'header': encodedAuthHeaders, 'payload': base64.encode(utf8.encode(json.encode(_emptyBody))), }; - // Conditionally format the URI for a) AppSync domain b) custom domain. In either - // case, add the auth query parameters. - final endpointUriHost = Uri.parse(config.endpoint).host; + // Conditionally format the URI for a) AppSync domain b) custom domain. + var endpointUriHost = Uri.parse(config.endpoint).host; + String path; if (endpointUriHost.contains(_appSyncHostPortion) && endpointUriHost.endsWith('amazonaws.com')) { // AppSync domain that contains "appsync-api" and ends with "amazonaws.com." - // Replace "appsync-api" with "appsync-realtime-api," append "/graphql" and - // add formatted auth query parameters. - final appSyncRealtimeUri = Uri.parse( - config.endpoint - .replaceFirst(_appSyncHostPortion, _appSyncRealtimeHostPortion), - ); - return Uri( - scheme: _wssScheme, - host: appSyncRealtimeUri.host, - path: 'graphql', - ).replace( - queryParameters: authQueryParameters, + // Replace "appsync-api" with "appsync-realtime-api," append "/graphql." + endpointUriHost = endpointUriHost.replaceFirst( + _appSyncHostPortion, + _appSyncRealtimeHostPortion, ); + path = 'graphql'; + } else { + // Custom domain, append "graphql/realtime" to the path like on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. + path = 'graphql/realtime'; } - // Custom domain (not AppSync); format URL as documented on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. - // Appends "/graphql/realtime" path in addition to auth query parameters. + // Return wss URI with auth query parameters. return Uri( - scheme: _wssScheme, + scheme: 'wss', host: endpointUriHost, - path: 'graphql/realtime', + path: path, ).replace( queryParameters: authQueryParameters, ); From 5b4b8d34238ebb6c52b0df51e3369ef45ce40f50 Mon Sep 17 00:00:00 2001 From: Travis Sheppard Date: Thu, 8 Jun 2023 11:52:20 -0700 Subject: [PATCH 4/4] add consts --- .../lib/src/decorators/web_socket_auth_utils.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart index a2a4438a1f..f0ce2969fc 100644 --- a/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart +++ b/packages/api/amplify_api_dart/lib/src/decorators/web_socket_auth_utils.dart @@ -13,6 +13,9 @@ import 'package:meta/meta.dart'; const _appSyncHostPortion = 'appsync-api'; const _appSyncRealtimeHostPortion = 'appsync-realtime-api'; +const _appSyncHostSuffix = 'amazonaws.com'; +const _appSyncPath = 'graphql'; +const _customDomainPath = 'graphql/realtime'; // Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html. const _requiredHeaders = { @@ -48,17 +51,17 @@ Future generateConnectionUri( var endpointUriHost = Uri.parse(config.endpoint).host; String path; if (endpointUriHost.contains(_appSyncHostPortion) && - endpointUriHost.endsWith('amazonaws.com')) { + endpointUriHost.endsWith(_appSyncHostSuffix)) { // AppSync domain that contains "appsync-api" and ends with "amazonaws.com." // Replace "appsync-api" with "appsync-realtime-api," append "/graphql." endpointUriHost = endpointUriHost.replaceFirst( _appSyncHostPortion, _appSyncRealtimeHostPortion, ); - path = 'graphql'; + path = _appSyncPath; } else { // Custom domain, append "graphql/realtime" to the path like on https://docs.aws.amazon.com/appsync/latest/devguide/custom-domain-name.html. - path = 'graphql/realtime'; + path = _customDomainPath; } // Return wss URI with auth query parameters. return Uri(