1<?php
2namespace Aws\Endpoint;
3
4use ArrayAccess;
5use Aws\HasDataTrait;
6use Aws\Sts\RegionalEndpoints\ConfigurationProvider;
7use Aws\S3\RegionalEndpoint\ConfigurationProvider as S3ConfigurationProvider;
8use InvalidArgumentException as Iae;
9
10/**
11 * Default implementation of an AWS partition.
12 */
13final class Partition implements ArrayAccess, PartitionInterface
14{
15    use HasDataTrait;
16
17    private $stsLegacyGlobalRegions = [
18        'ap-northeast-1',
19        'ap-south-1',
20        'ap-southeast-1',
21        'ap-southeast-2',
22        'aws-global',
23        'ca-central-1',
24        'eu-central-1',
25        'eu-north-1',
26        'eu-west-1',
27        'eu-west-2',
28        'eu-west-3',
29        'sa-east-1',
30        'us-east-1',
31        'us-east-2',
32        'us-west-1',
33        'us-west-2',
34    ];
35
36    /**
37     * The partition constructor accepts the following options:
38     *
39     * - `partition`: (string, required) The partition name as specified in an
40     *   ARN (e.g., `aws`)
41     * - `partitionName`: (string) The human readable name of the partition
42     *   (e.g., "AWS Standard")
43     * - `dnsSuffix`: (string, required) The DNS suffix of the partition. This
44     *   value is used to determine how endpoints in the partition are resolved.
45     * - `regionRegex`: (string) A PCRE regular expression that specifies the
46     *   pattern that region names in the endpoint adhere to.
47     * - `regions`: (array, required) A map of the regions in the partition.
48     *   Each key is the region as present in a hostname (e.g., `us-east-1`),
49     *   and each value is a structure containing region information.
50     * - `defaults`: (array) A map of default key value pairs to apply to each
51     *   endpoint of the partition. Any value in an `endpoint` definition will
52     *   supersede any values specified in `defaults`.
53     * - `services`: (array, required) A map of service endpoint prefix name
54     *   (the value found in a hostname) to information about the service.
55     *
56     * @param array $definition
57     *
58     * @throws Iae if any required options are missing
59     */
60    public function __construct(array $definition)
61    {
62        foreach (['partition', 'regions', 'services', 'dnsSuffix'] as $key) {
63            if (!isset($definition[$key])) {
64                throw new Iae("Partition missing required $key field");
65            }
66        }
67
68        $this->data = $definition;
69    }
70
71    public function getName()
72    {
73        return $this->data['partition'];
74    }
75
76    /**
77     * @internal
78     * @return mixed
79     */
80    public function getDnsSuffix()
81    {
82        return $this->data['dnsSuffix'];
83    }
84
85    public function isRegionMatch($region, $service)
86    {
87        if (isset($this->data['regions'][$region])
88            || isset($this->data['services'][$service]['endpoints'][$region])
89        ) {
90            return true;
91        }
92
93        if (isset($this->data['regionRegex'])) {
94            return (bool) preg_match(
95                "@{$this->data['regionRegex']}@",
96                $region
97            );
98        }
99
100        return false;
101    }
102
103    public function getAvailableEndpoints(
104        $service,
105        $allowNonRegionalEndpoints = false
106    ) {
107        if ($this->isServicePartitionGlobal($service)) {
108            return [$this->getPartitionEndpoint($service)];
109        }
110
111        if (isset($this->data['services'][$service]['endpoints'])) {
112            $serviceRegions = array_keys(
113                $this->data['services'][$service]['endpoints']
114            );
115
116            return $allowNonRegionalEndpoints
117                ? $serviceRegions
118                : array_intersect($serviceRegions, array_keys(
119                    $this->data['regions']
120                ));
121        }
122
123        return [];
124    }
125
126    public function __invoke(array $args = [])
127    {
128        $service = isset($args['service']) ? $args['service'] : '';
129        $region = isset($args['region']) ? $args['region'] : '';
130        $scheme = isset($args['scheme']) ? $args['scheme'] : 'https';
131        $options = isset($args['options']) ? $args['options'] : [];
132        $data = $this->getEndpointData($service, $region, $options);
133
134        return [
135            'endpoint' => "{$scheme}://" . $this->formatEndpoint(
136                    isset($data['hostname']) ? $data['hostname'] : '',
137                    $service,
138                    $region
139                ),
140            'signatureVersion' => $this->getSignatureVersion($data),
141            'signingRegion' => isset($data['credentialScope']['region'])
142                ? $data['credentialScope']['region']
143                : $region,
144            'signingName' => isset($data['credentialScope']['service'])
145                ? $data['credentialScope']['service']
146                : $service,
147        ];
148    }
149
150    private function getEndpointData($service, $region, $options)
151    {
152        $defaultRegion = $this->resolveRegion($service, $region, $options);
153        $data = isset($this->data['services'][$service]['endpoints'][$defaultRegion])
154            ? $this->data['services'][$service]['endpoints'][$defaultRegion]
155            : [];
156        $data += isset($this->data['services'][$service]['defaults'])
157            ? $this->data['services'][$service]['defaults']
158            : [];
159        $data += isset($this->data['defaults'])
160            ? $this->data['defaults']
161            : [];
162
163        return $data;
164    }
165
166    private function getSignatureVersion(array $data)
167    {
168        static $supportedBySdk = [
169            's3v4',
170            'v4',
171            'anonymous',
172        ];
173
174        $possibilities = array_intersect(
175            $supportedBySdk,
176            isset($data['signatureVersions'])
177                ? $data['signatureVersions']
178                : ['v4']
179        );
180
181        return array_shift($possibilities);
182    }
183
184    private function resolveRegion($service, $region, $options)
185    {
186        if (isset($this->data['services'][$service]['endpoints'][$region])
187            && $this->isFipsEndpointUsed($region)
188        ) {
189            return $region;
190        }
191
192        if ($this->isServicePartitionGlobal($service)
193            || $this->isStsLegacyEndpointUsed($service, $region, $options)
194            || $this->isS3LegacyEndpointUsed($service, $region, $options)
195        ) {
196            return $this->getPartitionEndpoint($service);
197        }
198
199        return $region;
200    }
201
202    private function isServicePartitionGlobal($service)
203    {
204        return isset($this->data['services'][$service]['isRegionalized'])
205            && false === $this->data['services'][$service]['isRegionalized']
206            && isset($this->data['services'][$service]['partitionEndpoint']);
207    }
208
209    /**
210     * STS legacy endpoints used for valid regions unless option is explicitly
211     * set to 'regional'
212     *
213     * @param string $service
214     * @param string $region
215     * @param array $options
216     * @return bool
217     */
218    private function isStsLegacyEndpointUsed($service, $region, $options)
219    {
220        return $service === 'sts'
221            && in_array($region, $this->stsLegacyGlobalRegions)
222            && (empty($options['sts_regional_endpoints'])
223                || ConfigurationProvider::unwrap(
224                    $options['sts_regional_endpoints']
225                )->getEndpointsType() !== 'regional'
226            );
227    }
228
229    /**
230     * S3 legacy us-east-1 endpoint used for valid regions unless option is explicitly
231     * set to 'regional'
232     *
233     * @param string $service
234     * @param string $region
235     * @param array $options
236     * @return bool
237     */
238    private function isS3LegacyEndpointUsed($service, $region, $options)
239    {
240        return $service === 's3'
241            && $region === 'us-east-1'
242            && (empty($options['s3_us_east_1_regional_endpoint'])
243                || S3ConfigurationProvider::unwrap(
244                    $options['s3_us_east_1_regional_endpoint']
245                )->getEndpointsType() !== 'regional'
246            );
247    }
248
249    private function getPartitionEndpoint($service)
250    {
251        return $this->data['services'][$service]['partitionEndpoint'];
252    }
253
254    private function formatEndpoint($template, $service, $region)
255    {
256        return strtr($template, [
257            '{service}' => $service,
258            '{region}' => $region,
259            '{dnsSuffix}' => $this->data['dnsSuffix'],
260        ]);
261    }
262
263    /**
264     * @param $region
265     * @return bool
266     */
267    private function isFipsEndpointUsed($region)
268    {
269        return strpos($region, "fips") !== false;
270    }
271}
272