PluginProbe
Gmail SMTP / trunk
Gmail SMTP vtrunk
1.2.3.21 1.2.3.20 trunk 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.9 1.2.0 1.2.3.14 1.2.3.15 1.2.3.16 1.2.3.18 1.2.3.5
gmail-smtp / google-api-php-client / vendor / google / auth / src / Credentials / GCECredentials.php

GCECredentials.php in Gmail SMTP trunk, at google-api-php-client/vendor/google/auth/src/Credentials/GCECredentials.php

722 lines 21.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /*
3 * Copyright 2015 Google Inc.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 namespace Google\Auth\Credentials;
19
20 use COM;
21 use com_exception;
22 use Google\Auth\CredentialsLoader;
23 use Google\Auth\GetQuotaProjectInterface;
24 use Google\Auth\HttpHandler\HttpClientCache;
25 use Google\Auth\HttpHandler\HttpHandlerFactory;
26 use Google\Auth\Iam;
27 use Google\Auth\IamSignerTrait;
28 use Google\Auth\ProjectIdProviderInterface;
29 use Google\Auth\SignBlobInterface;
30 use GuzzleHttp\Exception\ClientException;
31 use GuzzleHttp\Exception\ConnectException;
32 use GuzzleHttp\Exception\RequestException;
33 use GuzzleHttp\Exception\ServerException;
34 use GuzzleHttp\Psr7\Request;
35 use InvalidArgumentException;
36
37 /**
38 * GCECredentials supports authorization on Google Compute Engine.
39 *
40 * It can be used to authorize requests using the AuthTokenMiddleware, but will
41 * only succeed if being run on GCE:
42 *
43 * ```
44 * use Google\Auth\Credentials\GCECredentials;
45 * use Google\Auth\Middleware\AuthTokenMiddleware;
46 * use GuzzleHttp\Client;
47 * use GuzzleHttp\HandlerStack;
48 *
49 * $gce = new GCECredentials();
50 * $middleware = new AuthTokenMiddleware($gce);
51 * $stack = HandlerStack::create();
52 * $stack->push($middleware);
53 *
54 * $client = new Client([
55 * 'handler' => $stack,
56 * 'base_uri' => 'https://www.googleapis.com/taskqueue/v1beta2/projects/',
57 * 'auth' => 'google_auth'
58 * ]);
59 *
60 * $res = $client->get('myproject/taskqueues/myqueue');
61 * ```
62 */
63 class GCECredentials extends CredentialsLoader implements
64 SignBlobInterface,
65 ProjectIdProviderInterface,
66 GetQuotaProjectInterface
67 {
68 use IamSignerTrait;
69 use RegionalAccessBoundaryTrait;
70
71 // phpcs:disable
72 const cacheKey = 'GOOGLE_AUTH_PHP_GCE';
73 // phpcs:enable
74
75 /**
76 * The metadata IP address on appengine instances.
77 *
78 * The IP is used instead of the domain 'metadata' to avoid slow responses
79 * when not on Compute Engine.
80 */
81 const METADATA_IP = '169.254.169.254';
82
83 /**
84 * The metadata path of the default token.
85 */
86 const TOKEN_URI_PATH = 'v1/instance/service-accounts/default/token';
87
88 /**
89 * The metadata path of the default id token.
90 */
91 const ID_TOKEN_URI_PATH = 'v1/instance/service-accounts/default/identity';
92
93 /**
94 * The metadata path of the client ID.
95 */
96 const CLIENT_ID_URI_PATH = 'v1/instance/service-accounts/default/email';
97
98 /**
99 * The metadata path of the project ID.
100 */
101 const PROJECT_ID_URI_PATH = 'v1/project/project-id';
102
103 /**
104 * The metadata path of the project ID.
105 */
106 const UNIVERSE_DOMAIN_URI_PATH = 'v1/universe/universe-domain';
107
108 /**
109 * The header whose presence indicates GCE presence.
110 */
111 const FLAVOR_HEADER = 'Metadata-Flavor';
112
113 /**
114 * The Linux file which contains the product name.
115 */
116 private const GKE_PRODUCT_NAME_FILE = '/sys/class/dmi/id/product_name';
117
118 /**
119 * The Windows Registry key path to the product name
120 */
121 private const WINDOWS_REGISTRY_KEY_PATH = 'HKEY_LOCAL_MACHINE\\SYSTEM\\HardwareConfig\\Current\\';
122
123 /**
124 * The Windows registry key name for the product name
125 */
126 private const WINDOWS_REGISTRY_KEY_NAME = 'SystemProductName';
127
128 /**
129 * The Name of the product expected from the windows registry
130 */
131 private const PRODUCT_NAME = 'Google';
132
133 private const CRED_TYPE = 'mds';
134
135 /**
136 * Note: the explicit `timeout` and `tries` below is a workaround. The underlying
137 * issue is that resolving an unknown host on some networks will take
138 * 20-30 seconds; making this timeout short fixes the issue, but
139 * could lead to false negatives in the event that we are on GCE, but
140 * the metadata resolution was particularly slow. The latter case is
141 * "unlikely" since the expected 4-nines time is about 0.5 seconds.
142 * This allows us to limit the total ping maximum timeout to 1.5 seconds
143 * for developer desktop scenarios.
144 */
145 const MAX_COMPUTE_PING_TRIES = 3;
146 const COMPUTE_PING_CONNECTION_TIMEOUT_S = 0.5;
147
148 /**
149 * Flag used to ensure that the onGCE test is only done once;.
150 *
151 * @var bool
152 */
153 private $hasCheckedOnGce = false;
154
155 /**
156 * Flag that stores the value of the onGCE check.
157 *
158 * @var bool
159 */
160 private $isOnGce = false;
161
162 /**
163 * Result of fetchAuthToken.
164 *
165 * @var array<mixed>
166 */
167 protected $lastReceivedToken;
168
169 /**
170 * @var string|null
171 */
172 private $clientName;
173
174 /**
175 * @var string|null
176 */
177 private $projectId;
178
179 /**
180 * @var string
181 */
182 private $tokenUri;
183
184 /**
185 * @var string
186 */
187 private $targetAudience;
188
189 /**
190 * @var string|null
191 */
192 private $quotaProject;
193
194 /**
195 * @var string|null
196 */
197 private $serviceAccountIdentity;
198
199 /**
200 * @var string
201 */
202 private ?string $universeDomain;
203
204 /**
205 * @param Iam|null $iam [optional] An IAM instance.
206 * @param string|string[] $scope [optional] the scope of the access request,
207 * expressed either as an array or as a space-delimited string.
208 * @param string $targetAudience [optional] The audience for the ID token.
209 * @param string $quotaProject [optional] Specifies a project to bill for access
210 * charges associated with the request.
211 * @param string $serviceAccountIdentity [optional] Specify a service
212 * account identity name to use instead of "default".
213 * @param string|null $universeDomain [optional] Specify a universe domain to use
214 * instead of fetching one from the metadata server.
215 * @param bool $enableRegionalAccessBoundary Lookup and include the regional access boundary header.
216 */
217 public function __construct(
218 ?Iam $iam = null,
219 $scope = null,
220 $targetAudience = null,
221 $quotaProject = null,
222 $serviceAccountIdentity = null,
223 ?string $universeDomain = null,
224 bool $enableRegionalAccessBoundary = false
225 ) {
226 $this->iam = $iam;
227
228 if ($scope && $targetAudience) {
229 throw new InvalidArgumentException(
230 'Scope and targetAudience cannot both be supplied'
231 );
232 }
233
234 $tokenUri = self::getTokenUri($serviceAccountIdentity);
235 if ($scope) {
236 if (is_string($scope)) {
237 $scope = explode(' ', $scope);
238 }
239
240 $scope = implode(',', $scope);
241
242 $tokenUri = $tokenUri . '?scopes=' . $scope;
243 } elseif ($targetAudience) {
244 $tokenUri = self::getIdTokenUri($serviceAccountIdentity);
245 $tokenUri = $tokenUri . '?audience=' . $targetAudience;
246 $this->targetAudience = $targetAudience;
247 }
248
249 $this->tokenUri = $tokenUri;
250 $this->quotaProject = $quotaProject;
251 $this->serviceAccountIdentity = $serviceAccountIdentity;
252 $this->universeDomain = $universeDomain;
253 $this->enableRegionalAccessBoundary = $enableRegionalAccessBoundary;
254 }
255
256 /**
257 * The full uri for accessing the default token.
258 *
259 * @param string $serviceAccountIdentity [optional] Specify a service
260 * account identity name to use instead of "default".
261 * @return string
262 */
263 public static function getTokenUri($serviceAccountIdentity = null)
264 {
265 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
266 $base .= self::TOKEN_URI_PATH;
267
268 if ($serviceAccountIdentity) {
269 return str_replace(
270 '/default/',
271 '/' . $serviceAccountIdentity . '/',
272 $base
273 );
274 }
275 return $base;
276 }
277
278 /**
279 * The full uri for accessing the default service account.
280 *
281 * @param string $serviceAccountIdentity [optional] Specify a service
282 * account identity name to use instead of "default".
283 * @return string
284 */
285 public static function getClientNameUri($serviceAccountIdentity = null)
286 {
287 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
288 $base .= self::CLIENT_ID_URI_PATH;
289
290 if ($serviceAccountIdentity) {
291 return str_replace(
292 '/default/',
293 '/' . $serviceAccountIdentity . '/',
294 $base
295 );
296 }
297
298 return $base;
299 }
300
301 /**
302 * The full uri for accesesing the default identity token.
303 *
304 * @param string $serviceAccountIdentity [optional] Specify a service
305 * account identity name to use instead of "default".
306 * @return string
307 */
308 private static function getIdTokenUri($serviceAccountIdentity = null)
309 {
310 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
311 $base .= self::ID_TOKEN_URI_PATH;
312
313 if ($serviceAccountIdentity) {
314 return str_replace(
315 '/default/',
316 '/' . $serviceAccountIdentity . '/',
317 $base
318 );
319 }
320
321 return $base;
322 }
323
324 /**
325 * The full uri for accessing the default project ID.
326 *
327 * @return string
328 */
329 private static function getProjectIdUri()
330 {
331 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
332
333 return $base . self::PROJECT_ID_URI_PATH;
334 }
335
336 /**
337 * The full uri for accessing the default universe domain.
338 *
339 * @return string
340 */
341 private static function getUniverseDomainUri()
342 {
343 $base = 'http://' . self::METADATA_IP . '/computeMetadata/';
344
345 return $base . self::UNIVERSE_DOMAIN_URI_PATH;
346 }
347
348 /**
349 * Determines if this an App Engine Flexible instance, by accessing the
350 * GAE_INSTANCE environment variable.
351 *
352 * @return bool true if this an App Engine Flexible Instance, false otherwise
353 */
354 public static function onAppEngineFlexible()
355 {
356 return substr((string) getenv('GAE_INSTANCE'), 0, 4) === 'aef-';
357 }
358
359 /**
360 * Determines if this a GCE instance, by accessing the expected metadata
361 * host.
362 * If $httpHandler is not specified a the default HttpHandler is used.
363 *
364 * @param callable|null $httpHandler callback which delivers psr7 request
365 * @return bool True if this a GCEInstance, false otherwise
366 */
367 public static function onGce(?callable $httpHandler = null)
368 {
369 $httpHandler = $httpHandler
370 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
371
372 $checkUri = 'http://' . self::METADATA_IP;
373 for ($i = 1; $i <= self::MAX_COMPUTE_PING_TRIES; $i++) {
374 try {
375 // Comment from: oauth2client/client.py
376 //
377 // Note: the explicit `timeout` below is a workaround. The underlying
378 // issue is that resolving an unknown host on some networks will take
379 // 20-30 seconds; making this timeout short fixes the issue, but
380 // could lead to false negatives in the event that we are on GCE, but
381 // the metadata resolution was particularly slow. The latter case is
382 // "unlikely".
383 $resp = $httpHandler(
384 new Request(
385 'GET',
386 $checkUri,
387 [
388 self::FLAVOR_HEADER => 'Google',
389 self::$metricMetadataKey => self::getMetricsHeader('', 'mds')
390 ]
391 ),
392 ['timeout' => self::COMPUTE_PING_CONNECTION_TIMEOUT_S]
393 );
394
395 return $resp->getHeaderLine(self::FLAVOR_HEADER) == 'Google';
396 } catch (ClientException $e) {
397 } catch (ServerException $e) {
398 } catch (RequestException $e) {
399 } catch (ConnectException $e) {
400 }
401 }
402
403 if (PHP_OS === 'Windows' || PHP_OS === 'WINNT') {
404 return self::detectResidencyWindows(
405 self::WINDOWS_REGISTRY_KEY_PATH . self::WINDOWS_REGISTRY_KEY_NAME
406 );
407 }
408
409 // Detect GCE residency on Linux
410 return self::detectResidencyLinux(self::GKE_PRODUCT_NAME_FILE);
411 }
412
413 private static function detectResidencyLinux(string $productNameFile): bool
414 {
415 if (file_exists($productNameFile)) {
416 $productName = trim((string) file_get_contents($productNameFile));
417 return 0 === strpos($productName, self::PRODUCT_NAME);
418 }
419 return false;
420 }
421
422 private static function detectResidencyWindows(string $registryProductKey): bool
423 {
424 if (!class_exists(COM::class)) {
425 // the COM extension must be installed and enabled to detect Windows residency
426 // see https://www.php.net/manual/en/book.com.php
427 return false;
428 }
429
430 $shell = new COM('WScript.Shell');
431 $productName = null;
432
433 try {
434 $productName = $shell->regRead($registryProductKey);
435 } catch (com_exception) {
436 // This means that we tried to read a key that doesn't exist on the registry
437 // which might mean that it is a windows instance that is not on GCE
438 return false;
439 }
440
441 return 0 === strpos($productName, self::PRODUCT_NAME);
442 }
443
444 /**
445 * Implements FetchAuthTokenInterface#fetchAuthToken.
446 *
447 * Fetches the auth tokens from the GCE metadata host if it is available.
448 * If $httpHandler is not specified a the default HttpHandler is used.
449 *
450 * @param callable|null $httpHandler callback which delivers psr7 request
451 * @param array<mixed> $headers [optional] Headers to be inserted
452 * into the token endpoint request present.
453 *
454 * @return array<mixed> {
455 * A set of auth related metadata, based on the token type.
456 *
457 * @type string $access_token for access tokens
458 * @type int $expires_in for access tokens
459 * @type string $token_type for access tokens
460 * @type string $id_token for ID tokens
461 * }
462 * @throws \Exception
463 */
464 public function fetchAuthToken(?callable $httpHandler = null, array $headers = [])
465 {
466 $httpHandler = $httpHandler
467 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
468
469 if (!$this->hasCheckedOnGce) {
470 $this->isOnGce = self::onGce($httpHandler);
471 $this->hasCheckedOnGce = true;
472 }
473 if (!$this->isOnGce) {
474 return []; // return an empty array with no access token
475 }
476
477 $response = $this->getFromMetadata(
478 $httpHandler,
479 $this->tokenUri,
480 $this->applyTokenEndpointMetrics($headers, $this->targetAudience ? 'it' : 'at')
481 );
482
483 if ($this->targetAudience) {
484 return $this->lastReceivedToken = ['id_token' => $response];
485 }
486
487 if (null === $json = json_decode($response, true)) {
488 throw new \Exception('Invalid JSON response');
489 }
490
491 $json['expires_at'] = time() + $json['expires_in'];
492
493 // store this so we can retrieve it later
494 $this->lastReceivedToken = $json;
495
496 return $json;
497 }
498
499 /**
500 * Returns the Cache Key for the credential token.
501 * The format for the cache key is:
502 * TokenURI
503 *
504 * @return string
505 */
506 public function getCacheKey()
507 {
508 return $this->tokenUri;
509 }
510
511 /**
512 * @return array<mixed>|null
513 */
514 public function getLastReceivedToken()
515 {
516 if ($this->lastReceivedToken) {
517 if (array_key_exists('id_token', $this->lastReceivedToken)) {
518 return $this->lastReceivedToken;
519 }
520
521 return [
522 'access_token' => $this->lastReceivedToken['access_token'],
523 'expires_at' => $this->lastReceivedToken['expires_at']
524 ];
525 }
526
527 return null;
528 }
529
530 /**
531 * Get the client name from GCE metadata.
532 *
533 * Subsequent calls will return a cached value.
534 *
535 * @param callable|null $httpHandler callback which delivers psr7 request
536 * @return string
537 */
538 public function getClientName(?callable $httpHandler = null)
539 {
540 if ($this->clientName) {
541 return $this->clientName;
542 }
543
544 $httpHandler = $httpHandler
545 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
546
547 if (!$this->hasCheckedOnGce) {
548 $this->isOnGce = self::onGce($httpHandler);
549 $this->hasCheckedOnGce = true;
550 }
551
552 if (!$this->isOnGce) {
553 return '';
554 }
555
556 $this->clientName = $this->getFromMetadata(
557 $httpHandler,
558 self::getClientNameUri($this->serviceAccountIdentity)
559 );
560
561 return $this->clientName;
562 }
563
564 /**
565 * Fetch the default Project ID from compute engine.
566 *
567 * Returns null if called outside GCE.
568 *
569 * @param callable|null $httpHandler Callback which delivers psr7 request
570 * @return string|null
571 */
572 public function getProjectId(?callable $httpHandler = null)
573 {
574 if ($this->projectId) {
575 return $this->projectId;
576 }
577
578 $httpHandler = $httpHandler
579 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
580
581 if (!$this->hasCheckedOnGce) {
582 $this->isOnGce = self::onGce($httpHandler);
583 $this->hasCheckedOnGce = true;
584 }
585
586 if (!$this->isOnGce) {
587 return null;
588 }
589
590 $this->projectId = $this->getFromMetadata($httpHandler, self::getProjectIdUri());
591 return $this->projectId;
592 }
593
594 /**
595 * Fetch the default universe domain from the metadata server.
596 *
597 * @param callable|null $httpHandler Callback which delivers psr7 request
598 * @return string
599 */
600 public function getUniverseDomain(?callable $httpHandler = null): string
601 {
602 if (null !== $this->universeDomain) {
603 return $this->universeDomain;
604 }
605
606 $httpHandler = $httpHandler
607 ?: HttpHandlerFactory::build(HttpClientCache::getHttpClient());
608
609 if (!$this->hasCheckedOnGce) {
610 $this->isOnGce = self::onGce($httpHandler);
611 $this->hasCheckedOnGce = true;
612 }
613
614 try {
615 $this->universeDomain = $this->getFromMetadata(
616 $httpHandler,
617 self::getUniverseDomainUri()
618 );
619 } catch (ClientException $e) {
620 // If the metadata server exists, but returns a 404 for the universe domain, the auth
621 // libraries should safely assume this is an older metadata server running in GCU, and
622 // should return the default universe domain.
623 if (!$e->hasResponse() || 404 != $e->getResponse()->getStatusCode()) {
624 throw $e;
625 }
626 $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
627 }
628
629 // We expect in some cases the metadata server will return an empty string for the universe
630 // domain. In this case, the auth library MUST return the default universe domain.
631 if ('' === $this->universeDomain) {
632 $this->universeDomain = self::DEFAULT_UNIVERSE_DOMAIN;
633 }
634
635 return $this->universeDomain;
636 }
637
638 /**
639 * Updates metadata with the authorization token.
640 *
641 * @param array<mixed> $metadata metadata hashmap
642 * @param string $authUri optional auth uri
643 * @param callable|null $httpHandler callback which delivers psr7 request
644 * @return array<mixed> updated metadata hashmap
645 */
646 public function updateMetadata(
647 $metadata,
648 $authUri = null,
649 ?callable $httpHandler = null
650 ) {
651 $metadata = parent::updateMetadata($metadata, $authUri, $httpHandler);
652
653 if ($this->enableRegionalAccessBoundary) {
654 $serviceAccountEmail = $this->getClientName($httpHandler);
655 if (preg_match('/^[^@]+@[^@]+\.[^@]+$/', $serviceAccountEmail)) {
656 $metadata = $this->updateRegionalAccessBoundaryMetadata(
657 $metadata,
658 $this->buildRegionalAccessBoundaryLookupUrl($serviceAccountEmail),
659 $this->getUniverseDomain($httpHandler),
660 $httpHandler,
661 );
662 }
663 }
664
665 return $metadata;
666 }
667
668 /**
669 * Fetch the value of a GCE metadata server URI.
670 *
671 * @param callable $httpHandler An HTTP Handler to deliver PSR7 requests.
672 * @param string $uri The metadata URI.
673 * @param array<mixed> $headers [optional] If present, add these headers to the token
674 * endpoint request.
675 *
676 * @return string
677 */
678 private function getFromMetadata(callable $httpHandler, $uri, array $headers = [])
679 {
680 $resp = $httpHandler(
681 new Request(
682 'GET',
683 $uri,
684 [self::FLAVOR_HEADER => 'Google'] + $headers
685 )
686 );
687
688 return (string) $resp->getBody();
689 }
690
691 /**
692 * Get the quota project used for this API request
693 *
694 * @return string|null
695 */
696 public function getQuotaProject()
697 {
698 return $this->quotaProject;
699 }
700
701 /**
702 * Set whether or not we've already checked the GCE environment.
703 *
704 * @param bool $isOnGce
705 *
706 * @return void
707 */
708 public function setIsOnGce($isOnGce)
709 {
710 // Implicitly set hasCheckedGce to true
711 $this->hasCheckedOnGce = true;
712
713 // Set isOnGce
714 $this->isOnGce = $isOnGce;
715 }
716
717 protected function getCredType(): string
718 {
719 return self::CRED_TYPE;
720 }
721 }
722