diff --git a/.changes/nextrelease/endpoint-url.json b/.changes/nextrelease/endpoint-url.json new file mode 100644 index 0000000000..93f541aecd --- /dev/null +++ b/.changes/nextrelease/endpoint-url.json @@ -0,0 +1,7 @@ +[ + { + "type": "feature", + "category": "", + "description": "Adds support for service-specific endpoint url configuration." + } +] diff --git a/src/ClientResolver.php b/src/ClientResolver.php index 0d7239eda1..69f4439d91 100644 --- a/src/ClientResolver.php +++ b/src/ClientResolver.php @@ -83,11 +83,19 @@ class ClientResolver 'doc' => 'Set to true to disable host prefix injection logic for services that use it. This disables the entire prefix injection, including the portions supplied by user-defined parameters. Setting this flag will have no effect on services that do not use host prefix injection.', 'default' => false, ], + 'ignore_configured_endpoint_urls' => [ + 'type' => 'value', + 'valid' => ['bool'], + 'doc' => 'Set to true to disable endpoint urls configured using `AWS_ENDPOINT_URL` and `endpoint_url` shared config option.', + 'fn' => [__CLASS__, '_apply_ignore_configured_endpoint_urls'], + 'default' => [__CLASS__, '_default_ignore_configured_endpoint_urls'], + ], 'endpoint' => [ 'type' => 'value', 'valid' => ['string'], 'doc' => 'The full URI of the webservice. This is only required when connecting to a custom endpoint (e.g., a local version of S3).', 'fn' => [__CLASS__, '_apply_endpoint'], + 'default' => [__CLASS__, '_default_endpoint'] ], 'region' => [ 'type' => 'value', @@ -993,6 +1001,11 @@ public static function _apply_user_agent($inputUserAgent, array &$args, HandlerL public static function _apply_endpoint($value, array &$args, HandlerList $list) { + if (empty($value)) { + unset($args['endpoint']); + return; + } + $args['endpoint'] = $value; } @@ -1116,6 +1129,55 @@ public static function _default_signing_region(array &$args) : $args['region']; } + public static function _apply_ignore_configured_endpoint_urls($value, array &$args) + { + $args['config']['ignore_configured_endpoint_urls'] = $value; + } + + public static function _default_ignore_configured_endpoint_urls(array &$args) + { + return ConfigurationResolver::resolve( + 'ignore_configured_endpoint_urls', + false, + 'bool', + $args + ); + } + + public static function _default_endpoint(array &$args) + { + if ($args['config']['ignore_configured_endpoint_urls'] + || !self::isValidService($args['service']) + ) { + return ''; + } + + $serviceIdentifier = \Aws\manifest($args['service'])['serviceIdentifier']; + $value = ConfigurationResolver::resolve( + 'endpoint_url_' . $serviceIdentifier, + '', + 'string', + $args + [ + 'ini_resolver_options' => [ + 'section' => 'services', + 'subsection' => $serviceIdentifier, + 'key' => 'endpoint_url' + ] + ] + ); + + if (empty($value)) { + $value = ConfigurationResolver::resolve( + 'endpoint_url', + '', + 'string', + $args + ); + } + + return $value; + } + public static function _apply_region($value, array &$args) { if (empty($value)) { diff --git a/src/CloudSearchDomain/CloudSearchDomainClient.php b/src/CloudSearchDomain/CloudSearchDomainClient.php index fc670cb373..a5971b7830 100644 --- a/src/CloudSearchDomain/CloudSearchDomainClient.php +++ b/src/CloudSearchDomain/CloudSearchDomainClient.php @@ -3,6 +3,7 @@ use Aws\AwsClient; use Aws\CommandInterface; +use Aws\HandlerList; use GuzzleHttp\Psr7\Uri; use Psr\Http\Message\RequestInterface; use GuzzleHttp\Psr7; @@ -35,6 +36,7 @@ public static function getArguments() // (e.g. http://search-blah.{region}.cloudsearch.amazonaws.com) return explode('.', new Uri($args['endpoint']))[1]; }; + unset($args['endpoint']['default']); return $args; } diff --git a/src/Configuration/ConfigurationResolver.php b/src/Configuration/ConfigurationResolver.php index b8984952ff..a08595a75d 100644 --- a/src/Configuration/ConfigurationResolver.php +++ b/src/Configuration/ConfigurationResolver.php @@ -21,8 +21,7 @@ class ConfigurationResolver * to retrieve value from the environment or ini file. * @param mixed $defaultValue * @param string $expectedType The expected type of the retrieved value. - * @param array $config - * @param array $additionalArgs + * @param array $config additional configuration options. * * @return mixed */ @@ -33,6 +32,10 @@ public static function resolve( $config = [] ) { + $iniOptions = isset($config['ini_resolver_options']) + ? $config['ini_resolver_options'] + : []; + $envValue = self::env($key, $expectedType); if (!is_null($envValue)) { return $envValue; @@ -41,7 +44,13 @@ public static function resolve( if (!isset($config['use_aws_shared_config_files']) || $config['use_aws_shared_config_files'] != false ) { - $iniValue = self::ini($key, $expectedType); + $iniValue = self::ini( + $key, + $expectedType, + null, + null, + $iniOptions + ); if(!is_null($iniValue)) { return $iniValue; } @@ -89,8 +98,13 @@ public static function env($key, $expectedType) * * @return null | mixed */ - public static function ini($key, $expectedType, $profile = null, $filename = null) - { + public static function ini( + $key, + $expectedType, + $profile = null, + $filename = null, + $options = [] + ){ $filename = $filename ?: (self::getDefaultConfigFilename()); $profile = $profile ?: (getenv(self::ENV_PROFILE) ?: 'default'); @@ -100,6 +114,20 @@ public static function ini($key, $expectedType, $profile = null, $filename = nul // Use INI_SCANNER_NORMAL instead of INI_SCANNER_TYPED for PHP 5.5 compatibility //TODO change after deprecation $data = @\Aws\parse_ini_file($filename, true, INI_SCANNER_NORMAL); + + if (isset($options['section']) + && isset($options['subsection']) + && isset($options['key'])) + { + return self::retrieveValueFromIniSubsection( + $data, + $profile, + $filename, + $expectedType, + $options + ); + } + if ($data === false || !isset($data[$profile]) || !isset($data[$profile][$key]) @@ -177,4 +205,46 @@ private static function convertType($value, $type) } return $value; } -} \ No newline at end of file + + /** + * Normalizes string values pulled out of ini files and + * environment variables. + * + * @param array $data The data retrieved the ini file + * @param string $profile The specified ini profile + * @param string $filename The full path to the ini file + * @param array $options Additional arguments passed to the configuration resolver + * + * @return mixed + */ + private static function retrieveValueFromIniSubsection( + $data, + $profile, + $filename, + $expectedType, + $options + ){ + $section = $options['section']; + if ($data === false + || !isset($data[$profile][$section]) + || !isset($data["{$section} {$data[$profile][$section]}"]) + ) { + return null; + } + + $services_section = \Aws\parse_ini_section_with_subsections( + $filename, + "services {$data[$profile]['services']}" + ); + + if (!isset($services_section[$options['subsection']][$options['key']]) + ) { + return null; + } + + return self::convertType( + $services_section[$options['subsection']][$options['key']], + $expectedType + ); + } +} diff --git a/src/functions.php b/src/functions.php index 342acd431b..4533728021 100644 --- a/src/functions.php +++ b/src/functions.php @@ -503,6 +503,67 @@ function boolean_value($input) return null; } +/** + * Parses ini sections with subsections (i.e. the service section) + * + * @param $filename + * @param $filename + * @return array + */ +function parse_ini_section_with_subsections($filename, $section_name) { + $config = []; + $stream = fopen($filename, 'r'); + + if (!$stream) { + return $config; + } + + $current_subsection = ''; + + while (!feof($stream)) { + $line = trim(fgets($stream)); + + if (empty($line) || in_array($line[0], [';', '#'])) { + continue; + } + + if (preg_match('/^\[.*\]$/', $line) + && trim($line, '[]') === $section_name) + { + while (!feof($stream)) { + $line = trim(fgets($stream)); + + if (empty($line) || in_array($line[0], [';', '#'])) { + continue; + } + + if (preg_match('/^\[.*\]$/', $line) + && trim($line, '[]') === $section_name) + { + continue; + } elseif (strpos($line, '[') === 0) { + break; + } + + if (strpos($line, ' = ') !== false) { + list($key, $value) = explode(' = ', $line, 2); + if (empty($current_subsection)) { + $config[$key] = $value; + } else { + $config[$current_subsection][$key] = $value; + } + } else { + $current_subsection = trim(str_replace('=', '', $line)); + $config[$current_subsection] = []; + } + } + } + } + + fclose($stream); + return $config; +} + /** * Checks if an input is a valid epoch time * diff --git a/tests/AwsClientTest.php b/tests/AwsClientTest.php index 75b7b675d2..e473b1cca0 100644 --- a/tests/AwsClientTest.php +++ b/tests/AwsClientTest.php @@ -472,7 +472,8 @@ public function testVerifyGetConfig() 'use_fips_endpoint' => new FipsConfiguration(false), 'use_dual_stack_endpoint' => new DualStackConfiguration(false, "foo"), 'disable_request_compression' => false, - 'request_min_compression_size_bytes' => 10240 + 'request_min_compression_size_bytes' => 10240, + 'ignore_configured_endpoint_urls' => false ], $client->getConfig() ); diff --git a/tests/ClientResolverTest.php b/tests/ClientResolverTest.php index e2b2704a4a..322ddefef1 100644 --- a/tests/ClientResolverTest.php +++ b/tests/ClientResolverTest.php @@ -1420,6 +1420,8 @@ public function testMinCompressionSizeDefault() * @param $ini * @param $env * @param $expected + * @param $configKey + * @param $configType */ public function testConfigResolutionOrder($ini, $env, $expected, $configKey, $configType) { @@ -1438,9 +1440,9 @@ public function testConfigResolutionOrder($ini, $env, $expected, $configKey, $co $r = new ClientResolver(ClientResolver::getDefaultArguments()); $conf = $r->resolve([ 'service' => 's3', - 'version' => 'latest' + 'region' => $configKey === 'region' ? null : 'x' ], new HandlerList()); - + if ($configType === 'args') { $this->assertEquals($conf[$configKey], $expected); } else { @@ -1477,7 +1479,94 @@ public function configResolutionProvider() 'foo-region', 'region', 'args' - ] + ], + [ + << 'AWS_ENDPOINT_URL_S3', 'value' => 'https://test.com'], + 'https://test.com', + 'endpoint', + 'args' + ], + [ + << 'AWS_ENDPOINT_URL', 'value' => 'https://baz.com'], + 'https://baz.com', + 'endpoint', + 'args' + ], + [ + << 's3', + 'region' => 'x', + 'version' => 'latest', + 'ignore_configured_endpoint_urls' => true + ], new HandlerList()); + $this->assertFalse(isset($conf['config']['endpoint'])); + unlink($dir . '/config'); + putenv("HOME=$home"); + putenv('AWS_ENDPOINT_URL' . '='); + putenv('AWS_ENDPOINT_URL_S3' . '='); + } } diff --git a/tests/ConfigurationResolverTest.php b/tests/ConfigurationResolverTest.php index f963bba0b4..0cc8461fef 100644 --- a/tests/ConfigurationResolverTest.php +++ b/tests/ConfigurationResolverTest.php @@ -23,7 +23,6 @@ class ConfigurationResolverTest extends TestCase foo_configuration_option = 15 EOT; - private $stringIniFile = <<assertFalse($result); unlink($dir . '/config'); } -} \ No newline at end of file + + public function testResolvesServiceEnv() + { + $dir = $this->clearEnv(); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL_S3' + . '=' + . 'https://test.com' + ); + file_put_contents($dir . '/config', $this->servicesIniFile); + putenv('HOME=' . dirname($dir)); + $result = ConfigurationResolver::resolve( + 'endpoint_url_s3', + '', + 'string', + [ + 'config_resolver_options' => [ + 'service' => 's3', + 'key' => 'endpoint_url' + ] + ] + ); + $this->assertSame('https://test.com', $result); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL_S3' + . '=' + ); + } + + public function testResolvesServiceIni() + { + $dir = $this->clearEnv(); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL' + . '=' + . 'https://test.com' + ); + file_put_contents($dir . '/config', $this->servicesIniFile); + putenv('HOME=' . dirname($dir)); + $result = ConfigurationResolver::resolve( + 'endpoint_url_s3', + '', + 'string', + [ + 'ini_resolver_options' => [ + 'section' => 'services', + 'subsection' => 's3', + 'key' => 'endpoint_url' + ] + ] + ); + $this->assertSame('https://exmaple.com', $result); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL' + . '=' + ); + } + + /** + * @dataProvider duplicateIniFileProvider + */ + public function testResolvesServiceIniWithDuplicateSections($ini) + { + $dir = $this->clearEnv(); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL' + . '=' + . 'https://test.com' + ); + file_put_contents($dir . '/config', $ini); + putenv('HOME=' . dirname($dir)); + $result = ConfigurationResolver::resolve( + 'endpoint_url_s3', + '', + 'string', + [ + 'ini_resolver_options' => [ + 'section' => 'services', + 'subsection' => 's3', + 'key' => 'endpoint_url' + ] + ] + ); + $this->assertSame('https://exmaple.com', $result); + putenv( + ConfigurationResolver::$envPrefix + . 'ENDPOINT_URL' + . '=' + ); + } + + public function duplicateIniFileProvider() + { + return [ + [ + <<assertEquals( + $expected, + Aws\parse_ini_section_with_subsections($tmpFile, 'services my-services') + ); + unlink($tmpFile); + } + + public function getIniFileServiceTestCases() + { + return [ + [ + << [ + 'endpoint_url' => 'https://exmaple.com' + ], + 'elastic_beanstalk' => [ + 'endpoint_url' => 'https://exmaple.com', + ] + ] + ] + ]; + } }