Configuration.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. <?php
  2. declare(strict_types=1);
  3. namespace AsyncAws\Core;
  4. use AsyncAws\Core\Credentials\IniFileLoader;
  5. use AsyncAws\Core\Exception\InvalidArgument;
  6. /**
  7. * Helper object that holds all configuration to the API.
  8. *
  9. * @author Tobias Nyholm <tobias.nyholm@gmail.com>
  10. * @author Jérémy Derussé <jeremy@derusse.com>
  11. */
  12. final class Configuration
  13. {
  14. public const DEFAULT_REGION = 'us-east-1';
  15. public const OPTION_REGION = 'region';
  16. public const OPTION_DEBUG = 'debug';
  17. public const OPTION_PROFILE = 'profile';
  18. public const OPTION_ACCESS_KEY_ID = 'accessKeyId';
  19. public const OPTION_SECRET_ACCESS_KEY = 'accessKeySecret';
  20. public const OPTION_SESSION_TOKEN = 'sessionToken';
  21. public const OPTION_SHARED_CREDENTIALS_FILE = 'sharedCredentialsFile';
  22. public const OPTION_SHARED_CONFIG_FILE = 'sharedConfigFile';
  23. public const OPTION_ENDPOINT = 'endpoint';
  24. public const OPTION_ROLE_ARN = 'roleArn';
  25. public const OPTION_WEB_IDENTITY_TOKEN_FILE = 'webIdentityTokenFile';
  26. public const OPTION_ROLE_SESSION_NAME = 'roleSessionName';
  27. public const OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI = 'containerCredentialsRelativeUri';
  28. public const OPTION_ENDPOINT_DISCOVERY_ENABLED = 'endpointDiscoveryEnabled';
  29. // S3 specific option
  30. public const OPTION_PATH_STYLE_ENDPOINT = 'pathStyleEndpoint';
  31. public const OPTION_SEND_CHUNKED_BODY = 'sendChunkedBody';
  32. private const AVAILABLE_OPTIONS = [
  33. self::OPTION_REGION => true,
  34. self::OPTION_DEBUG => true,
  35. self::OPTION_PROFILE => true,
  36. self::OPTION_ACCESS_KEY_ID => true,
  37. self::OPTION_SECRET_ACCESS_KEY => true,
  38. self::OPTION_SESSION_TOKEN => true,
  39. self::OPTION_SHARED_CREDENTIALS_FILE => true,
  40. self::OPTION_SHARED_CONFIG_FILE => true,
  41. self::OPTION_ENDPOINT => true,
  42. self::OPTION_ROLE_ARN => true,
  43. self::OPTION_WEB_IDENTITY_TOKEN_FILE => true,
  44. self::OPTION_ROLE_SESSION_NAME => true,
  45. self::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI => true,
  46. self::OPTION_ENDPOINT_DISCOVERY_ENABLED => true,
  47. self::OPTION_PATH_STYLE_ENDPOINT => true,
  48. self::OPTION_SEND_CHUNKED_BODY => true,
  49. ];
  50. // Put fallback options into groups to avoid mixing of provided config and environment variables
  51. private const FALLBACK_OPTIONS = [
  52. [self::OPTION_REGION => ['AWS_REGION', 'AWS_DEFAULT_REGION']],
  53. [self::OPTION_PROFILE => ['AWS_PROFILE', 'AWS_DEFAULT_PROFILE']],
  54. [
  55. self::OPTION_ACCESS_KEY_ID => ['AWS_ACCESS_KEY_ID', 'AWS_ACCESS_KEY'],
  56. self::OPTION_SECRET_ACCESS_KEY => ['AWS_SECRET_ACCESS_KEY', 'AWS_SECRET_KEY'],
  57. self::OPTION_SESSION_TOKEN => 'AWS_SESSION_TOKEN',
  58. ],
  59. [self::OPTION_SHARED_CREDENTIALS_FILE => 'AWS_SHARED_CREDENTIALS_FILE'],
  60. [self::OPTION_SHARED_CONFIG_FILE => 'AWS_CONFIG_FILE'],
  61. [
  62. self::OPTION_ROLE_ARN => 'AWS_ROLE_ARN',
  63. self::OPTION_WEB_IDENTITY_TOKEN_FILE => 'AWS_WEB_IDENTITY_TOKEN_FILE',
  64. self::OPTION_ROLE_SESSION_NAME => 'AWS_ROLE_SESSION_NAME',
  65. ],
  66. [self::OPTION_CONTAINER_CREDENTIALS_RELATIVE_URI => 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'],
  67. [self::OPTION_ENDPOINT_DISCOVERY_ENABLED => ['AWS_ENDPOINT_DISCOVERY_ENABLED', 'AWS_ENABLE_ENDPOINT_DISCOVERY']],
  68. ];
  69. private const DEFAULT_OPTIONS = [
  70. self::OPTION_REGION => self::DEFAULT_REGION,
  71. self::OPTION_DEBUG => 'false',
  72. self::OPTION_PROFILE => 'default',
  73. self::OPTION_SHARED_CREDENTIALS_FILE => '~/.aws/credentials',
  74. self::OPTION_SHARED_CONFIG_FILE => '~/.aws/config',
  75. // https://docs.aws.amazon.com/general/latest/gr/rande.html
  76. self::OPTION_ENDPOINT => 'https://%service%.%region%.amazonaws.com',
  77. self::OPTION_PATH_STYLE_ENDPOINT => 'false',
  78. self::OPTION_SEND_CHUNKED_BODY => 'false',
  79. self::OPTION_ENDPOINT_DISCOVERY_ENABLED => 'false',
  80. ];
  81. private $data = [];
  82. private $userData = [];
  83. public static function create(array $options)
  84. {
  85. if (0 < \count($invalidOptions = array_diff_key($options, self::AVAILABLE_OPTIONS))) {
  86. throw new InvalidArgument(sprintf('Invalid option(s) "%s" passed to "%s::%s". ', implode('", "', array_keys($invalidOptions)), __CLASS__, __METHOD__));
  87. }
  88. // Force each option to be string or null
  89. $options = array_map(static function ($value) {
  90. return null !== $value ? (string) $value : $value;
  91. }, $options);
  92. $configuration = new self();
  93. $options = self::parseEnvironmentVariables($options);
  94. self::populateConfiguration($configuration, $options);
  95. $iniOptions = self::parseIniFiles($configuration);
  96. self::populateConfiguration($configuration, $iniOptions);
  97. return $configuration;
  98. }
  99. public static function optionExists(string $optionName): bool
  100. {
  101. return isset(self::AVAILABLE_OPTIONS[$optionName]);
  102. }
  103. /**
  104. * @psalm-return (
  105. * $name is
  106. * self::OPTION_REGION
  107. * |self::OPTION_DEBUG
  108. * |self::OPTION_PROFILE
  109. * |self::OPTION_SHARED_CREDENTIALS_FILE
  110. * |self::OPTION_SHARED_CONFIG_FILE
  111. * |self::OPTION_ENDPOINT
  112. * |self::OPTION_PATH_STYLE_ENDPOINT
  113. * |self::OPTION_SEND_CHUNKED_BODY
  114. * ? string
  115. * : ?string
  116. * )
  117. */
  118. public function get(string $name): ?string
  119. {
  120. if (!isset(self::AVAILABLE_OPTIONS[$name])) {
  121. throw new InvalidArgument(sprintf('Invalid option "%s" passed to "%s::%s". ', $name, __CLASS__, __METHOD__));
  122. }
  123. return $this->data[$name] ?? null;
  124. }
  125. public function has(string $name): bool
  126. {
  127. if (!isset(self::AVAILABLE_OPTIONS[$name])) {
  128. throw new InvalidArgument(sprintf('Invalid option "%s" passed to "%s::%s". ', $name, __CLASS__, __METHOD__));
  129. }
  130. return isset($this->data[$name]);
  131. }
  132. public function isDefault(string $name): bool
  133. {
  134. if (!isset(self::AVAILABLE_OPTIONS[$name])) {
  135. throw new InvalidArgument(sprintf('Invalid option "%s" passed to "%s::%s". ', $name, __CLASS__, __METHOD__));
  136. }
  137. return empty($this->userData[$name]);
  138. }
  139. private static function parseEnvironmentVariables(array $options): array
  140. {
  141. foreach (self::FALLBACK_OPTIONS as $fallbackGroup) {
  142. // prevent mixing env variables with config keys
  143. foreach ($fallbackGroup as $option => $envVariableNames) {
  144. if (isset($options[$option])) {
  145. continue 2;
  146. }
  147. }
  148. foreach ($fallbackGroup as $option => $envVariableNames) {
  149. // Read environment files
  150. $envVariableNames = (array) $envVariableNames;
  151. foreach ($envVariableNames as $envVariableName) {
  152. if (null !== $envVariableValue = EnvVar::get($envVariableName)) {
  153. $options[$option] = $envVariableValue;
  154. break;
  155. }
  156. }
  157. }
  158. }
  159. return $options;
  160. }
  161. /**
  162. * Look for "region" in the configured ini files.
  163. */
  164. private static function parseIniFiles(Configuration $configuration): array
  165. {
  166. $options = [];
  167. if (!$configuration->isDefault(self::OPTION_REGION)) {
  168. return $options;
  169. }
  170. $profilesData = (new IniFileLoader())->loadProfiles([
  171. $configuration->get(self::OPTION_SHARED_CREDENTIALS_FILE),
  172. $configuration->get(self::OPTION_SHARED_CONFIG_FILE),
  173. ]);
  174. if (empty($profilesData)) {
  175. return $options;
  176. }
  177. /** @var string $profile */
  178. $profile = $configuration->get(Configuration::OPTION_PROFILE);
  179. if (isset($profilesData[$profile]['region'])) {
  180. $options[self::OPTION_REGION] = $profilesData[$profile]['region'];
  181. }
  182. return $options;
  183. }
  184. /**
  185. * Add array options to the configuration object.
  186. */
  187. private static function populateConfiguration(Configuration $configuration, array $options): void
  188. {
  189. foreach ($options as $key => $value) {
  190. if (null !== $value) {
  191. $configuration->userData[$key] = true;
  192. }
  193. }
  194. // If we have not applied default before
  195. if (empty($configuration->data)) {
  196. foreach (self::DEFAULT_OPTIONS as $optionTrigger => $defaultValue) {
  197. if (isset($options[$optionTrigger])) {
  198. continue;
  199. }
  200. $options[$optionTrigger] = $defaultValue;
  201. }
  202. }
  203. $configuration->data = array_merge($configuration->data, $options);
  204. }
  205. }