1# Copyright 2014 Google Inc. All Rights Reserved. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""An OAuth2 client library. 16 17This library provides a client implementation of the OAuth2 protocol (see 18https://developers.google.com/storage/docs/authentication.html#oauth). 19 20**** Experimental API **** 21 22This module is experimental and is subject to modification or removal without 23notice. 24""" 25 26# This implementation is a wrapper around the oauth2client implementation 27# that implements caching of access tokens independent of refresh 28# tokens (in the python API client oauth2client, there is a single class that 29# encapsulates both refresh and access tokens). 30 31from __future__ import absolute_import 32 33import cgi 34import datetime 35import errno 36from hashlib import sha1 37import json 38import logging 39import os 40import socket 41import tempfile 42import threading 43import urllib 44 45if os.environ.get('USER_AGENT'): 46 import boto 47 boto.UserAgent += os.environ.get('USER_AGENT') 48 49from boto import config 50import httplib2 51from oauth2client import service_account 52from oauth2client.client import AccessTokenRefreshError 53from oauth2client.client import Credentials 54from oauth2client.client import EXPIRY_FORMAT 55from oauth2client.client import HAS_CRYPTO 56from oauth2client.client import OAuth2Credentials 57from retry_decorator.retry_decorator import retry as Retry 58import socks 59 60if HAS_CRYPTO: 61 from oauth2client.client import SignedJwtAssertionCredentials 62 63LOG = logging.getLogger('oauth2_client') 64 65# Lock used for checking/exchanging refresh token, so multithreaded 66# operation doesn't attempt concurrent refreshes. 67token_exchange_lock = threading.Lock() 68 69DEFAULT_SCOPE = 'https://www.googleapis.com/auth/devstorage.full_control' 70 71METADATA_SERVER = 'http://metadata.google.internal' 72 73META_TOKEN_URI = (METADATA_SERVER + '/computeMetadata/v1/instance/' 74 'service-accounts/default/token') 75 76META_HEADERS = { 77 'X-Google-Metadata-Request': 'True' 78} 79 80 81# Note: this is copied from gsutil's gslib.cred_types. It should be kept in 82# sync. Also note that this library does not use HMAC, but it's preserved from 83# gsutil's copy to maintain compatibility. 84class CredTypes(object): 85 HMAC = "HMAC" 86 OAUTH2_SERVICE_ACCOUNT = "OAuth 2.0 Service Account" 87 OAUTH2_USER_ACCOUNT = "Oauth 2.0 User Account" 88 GCE = "GCE" 89 90 91class Error(Exception): 92 """Base exception for the OAuth2 module.""" 93 pass 94 95 96class AuthorizationCodeExchangeError(Error): 97 """Error trying to exchange an authorization code into a refresh token.""" 98 pass 99 100 101class TokenCache(object): 102 """Interface for OAuth2 token caches.""" 103 104 def PutToken(self, key, value): 105 raise NotImplementedError 106 107 def GetToken(self, key): 108 raise NotImplementedError 109 110 111class NoopTokenCache(TokenCache): 112 """A stub implementation of TokenCache that does nothing.""" 113 114 def PutToken(self, key, value): 115 pass 116 117 def GetToken(self, key): 118 return None 119 120 121class InMemoryTokenCache(TokenCache): 122 """An in-memory token cache. 123 124 The cache is implemented by a python dict, and inherits the thread-safety 125 properties of dict. 126 """ 127 128 def __init__(self): 129 super(InMemoryTokenCache, self).__init__() 130 self.cache = dict() 131 132 def PutToken(self, key, value): 133 LOG.debug('InMemoryTokenCache.PutToken: key=%s', key) 134 self.cache[key] = value 135 136 def GetToken(self, key): 137 value = self.cache.get(key, None) 138 LOG.debug('InMemoryTokenCache.GetToken: key=%s%s present', 139 key, ' not' if value is None else '') 140 return value 141 142 143class FileSystemTokenCache(TokenCache): 144 """An implementation of a token cache that persists tokens on disk. 145 146 Each token object in the cache is stored in serialized form in a separate 147 file. The cache file's name can be configured via a path pattern that is 148 parameterized by the key under which a value is cached and optionally the 149 current processes uid as obtained by os.getuid(). 150 151 Since file names are generally publicly visible in the system, it is important 152 that the cache key does not leak information about the token's value. If 153 client code computes cache keys from token values, a cryptographically strong 154 one-way function must be used. 155 """ 156 157 def __init__(self, path_pattern=None): 158 """Creates a FileSystemTokenCache. 159 160 Args: 161 path_pattern: Optional string argument to specify the path pattern for 162 cache files. The argument should be a path with format placeholders 163 '%(key)s' and optionally '%(uid)s'. If the argument is omitted, the 164 default pattern 165 <tmpdir>/oauth2client-tokencache.%(uid)s.%(key)s 166 is used, where <tmpdir> is replaced with the system temp dir as 167 obtained from tempfile.gettempdir(). 168 """ 169 super(FileSystemTokenCache, self).__init__() 170 self.path_pattern = path_pattern 171 if not path_pattern: 172 self.path_pattern = os.path.join( 173 tempfile.gettempdir(), 'oauth2_client-tokencache.%(uid)s.%(key)s') 174 175 def CacheFileName(self, key): 176 uid = '_' 177 try: 178 # os.getuid() doesn't seem to work in Windows 179 uid = str(os.getuid()) 180 except: 181 pass 182 return self.path_pattern % {'key': key, 'uid': uid} 183 184 def PutToken(self, key, value): 185 """Serializes the value to the key's filename. 186 187 To ensure that written tokens aren't leaked to a different users, we 188 a) unlink an existing cache file, if any (to ensure we don't fall victim 189 to symlink attacks and the like), 190 b) create a new file with O_CREAT | O_EXCL (to ensure nobody is trying to 191 race us) 192 If either of these steps fail, we simply give up (but log a warning). Not 193 caching access tokens is not catastrophic, and failure to create a file 194 can happen for either of the following reasons: 195 - someone is attacking us as above, in which case we want to default to 196 safe operation (not write the token); 197 - another legitimate process is racing us; in this case one of the two 198 will win and write the access token, which is fine; 199 - we don't have permission to remove the old file or write to the 200 specified directory, in which case we can't recover 201 202 Args: 203 key: the hash key to store. 204 value: the access_token value to serialize. 205 """ 206 207 cache_file = self.CacheFileName(key) 208 LOG.debug('FileSystemTokenCache.PutToken: key=%s, cache_file=%s', 209 key, cache_file) 210 try: 211 os.unlink(cache_file) 212 except: 213 # Ignore failure to unlink the file; if the file exists and can't be 214 # unlinked, the subsequent open with O_CREAT | O_EXCL will fail. 215 pass 216 217 flags = os.O_RDWR | os.O_CREAT | os.O_EXCL 218 219 # Accommodate Windows; stolen from python2.6/tempfile.py. 220 if hasattr(os, 'O_NOINHERIT'): 221 flags |= os.O_NOINHERIT 222 if hasattr(os, 'O_BINARY'): 223 flags |= os.O_BINARY 224 225 try: 226 fd = os.open(cache_file, flags, 0600) 227 except (OSError, IOError) as e: 228 LOG.warning('FileSystemTokenCache.PutToken: ' 229 'Failed to create cache file %s: %s', cache_file, e) 230 return 231 f = os.fdopen(fd, 'w+b') 232 f.write(value.Serialize()) 233 f.close() 234 235 def GetToken(self, key): 236 """Returns a deserialized access token from the key's filename.""" 237 value = None 238 cache_file = self.CacheFileName(key) 239 240 try: 241 f = open(cache_file) 242 value = AccessToken.UnSerialize(f.read()) 243 f.close() 244 except (IOError, OSError) as e: 245 if e.errno != errno.ENOENT: 246 LOG.warning('FileSystemTokenCache.GetToken: ' 247 'Failed to read cache file %s: %s', cache_file, e) 248 except Exception as e: 249 LOG.warning('FileSystemTokenCache.GetToken: ' 250 'Failed to read cache file %s (possibly corrupted): %s', 251 cache_file, e) 252 253 LOG.debug('FileSystemTokenCache.GetToken: key=%s%s present (cache_file=%s)', 254 key, ' not' if value is None else '', cache_file) 255 return value 256 257 258class OAuth2Client(object): 259 """Common logic for OAuth2 clients.""" 260 261 def __init__(self, cache_key_base, access_token_cache=None, 262 datetime_strategy=datetime.datetime, auth_uri=None, 263 token_uri=None, disable_ssl_certificate_validation=False, 264 proxy_host=None, proxy_port=None, proxy_user=None, 265 proxy_pass=None, ca_certs_file=None): 266 # datetime_strategy is used to invoke utcnow() on; it is injected into the 267 # constructor for unit testing purposes. 268 self.auth_uri = auth_uri 269 self.token_uri = token_uri 270 self.cache_key_base = cache_key_base 271 self.datetime_strategy = datetime_strategy 272 self.access_token_cache = access_token_cache or InMemoryTokenCache() 273 self.disable_ssl_certificate_validation = disable_ssl_certificate_validation 274 self.ca_certs_file = ca_certs_file 275 if proxy_host and proxy_port: 276 self._proxy_info = httplib2.ProxyInfo(socks.PROXY_TYPE_HTTP, 277 proxy_host, 278 proxy_port, 279 proxy_user=proxy_user, 280 proxy_pass=proxy_pass, 281 proxy_rdns=True) 282 else: 283 self._proxy_info = None 284 285 def CreateHttpRequest(self): 286 return httplib2.Http( 287 ca_certs=self.ca_certs_file, 288 disable_ssl_certificate_validation=( 289 self.disable_ssl_certificate_validation), 290 proxy_info=self._proxy_info) 291 292 def GetAccessToken(self): 293 """Obtains an access token for this client. 294 295 This client's access token cache is first checked for an existing, 296 not-yet-expired access token. If none is found, the client obtains a fresh 297 access token from the OAuth2 provider's token endpoint. 298 299 Returns: 300 The cached or freshly obtained AccessToken. 301 Raises: 302 AccessTokenRefreshError if an error occurs. 303 """ 304 # Ensure only one thread at a time attempts to get (and possibly refresh) 305 # the access token. This doesn't prevent concurrent refresh attempts across 306 # multiple gsutil instances, but at least protects against multiple threads 307 # simultaneously attempting to refresh when gsutil -m is used. 308 token_exchange_lock.acquire() 309 try: 310 cache_key = self.CacheKey() 311 LOG.debug('GetAccessToken: checking cache for key %s', cache_key) 312 access_token = self.access_token_cache.GetToken(cache_key) 313 LOG.debug('GetAccessToken: token from cache: %s', access_token) 314 if access_token is None or access_token.ShouldRefresh(): 315 LOG.debug('GetAccessToken: fetching fresh access token...') 316 access_token = self.FetchAccessToken() 317 LOG.debug('GetAccessToken: fresh access token: %s', access_token) 318 self.access_token_cache.PutToken(cache_key, access_token) 319 return access_token 320 finally: 321 token_exchange_lock.release() 322 323 def CacheKey(self): 324 """Computes a cache key. 325 326 The cache key is computed as the SHA1 hash of the refresh token for user 327 accounts, or the hash of the gs_service_client_id for service accounts, 328 which satisfies the FileSystemTokenCache requirement that cache keys do not 329 leak information about token values. 330 331 Returns: 332 A hash key. 333 """ 334 h = sha1() 335 h.update(self.cache_key_base) 336 return h.hexdigest() 337 338 def GetAuthorizationHeader(self): 339 """Gets the access token HTTP authorization header value. 340 341 Returns: 342 The value of an Authorization HTTP header that authenticates 343 requests with an OAuth2 access token. 344 """ 345 return 'Bearer %s' % self.GetAccessToken().token 346 347 348class _BaseOAuth2ServiceAccountClient(OAuth2Client): 349 """Base class for OAuth2ServiceAccountClients. 350 351 Args: 352 client_id: The OAuth2 client ID of this client. 353 access_token_cache: An optional instance of a TokenCache. If omitted or 354 None, an InMemoryTokenCache is used. 355 auth_uri: The URI for OAuth2 authorization. 356 token_uri: The URI used to refresh access tokens. 357 datetime_strategy: datetime module strategy to use. 358 disable_ssl_certificate_validation: True if certifications should not be 359 validated. 360 proxy_host: An optional string specifying the host name of an HTTP proxy 361 to be used. 362 proxy_port: An optional int specifying the port number of an HTTP proxy 363 to be used. 364 proxy_user: An optional string specifying the user name for interacting 365 with the HTTP proxy. 366 proxy_pass: An optional string specifying the password for interacting 367 with the HTTP proxy. 368 ca_certs_file: The cacerts.txt file to use. 369 """ 370 371 def __init__(self, client_id, access_token_cache=None, auth_uri=None, 372 token_uri=None, datetime_strategy=datetime.datetime, 373 disable_ssl_certificate_validation=False, 374 proxy_host=None, proxy_port=None, proxy_user=None, 375 proxy_pass=None, ca_certs_file=None): 376 377 super(_BaseOAuth2ServiceAccountClient, self).__init__( 378 cache_key_base=client_id, auth_uri=auth_uri, token_uri=token_uri, 379 access_token_cache=access_token_cache, 380 datetime_strategy=datetime_strategy, 381 disable_ssl_certificate_validation=disable_ssl_certificate_validation, 382 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user, 383 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file) 384 self._client_id = client_id 385 386 def FetchAccessToken(self): 387 credentials = self.GetCredentials() 388 http = self.CreateHttpRequest() 389 credentials.refresh(http) 390 return AccessToken(credentials.access_token, credentials.token_expiry, 391 datetime_strategy=self.datetime_strategy) 392 393 394class OAuth2ServiceAccountClient(_BaseOAuth2ServiceAccountClient): 395 """An OAuth2 service account client using .p12 or .pem keys.""" 396 397 def __init__(self, client_id, private_key, password, 398 access_token_cache=None, auth_uri=None, token_uri=None, 399 datetime_strategy=datetime.datetime, 400 disable_ssl_certificate_validation=False, 401 proxy_host=None, proxy_port=None, proxy_user=None, 402 proxy_pass=None, ca_certs_file=None): 403 # Avoid long repeated kwargs list. 404 # pylint: disable=g-doc-args 405 """Creates an OAuth2ServiceAccountClient. 406 407 Args: 408 client_id: The OAuth2 client ID of this client. 409 private_key: The private key associated with this service account. 410 password: The private key password used for the crypto signer. 411 412 Keyword arguments match the _BaseOAuth2ServiceAccountClient class. 413 """ 414 # pylint: enable=g-doc-args 415 super(OAuth2ServiceAccountClient, self).__init__( 416 client_id, auth_uri=auth_uri, token_uri=token_uri, 417 access_token_cache=access_token_cache, 418 datetime_strategy=datetime_strategy, 419 disable_ssl_certificate_validation=disable_ssl_certificate_validation, 420 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user, 421 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file) 422 self._private_key = private_key 423 self._password = password 424 425 def GetCredentials(self): 426 if HAS_CRYPTO: 427 return SignedJwtAssertionCredentials( 428 self._client_id, self._private_key, scope=DEFAULT_SCOPE, 429 private_key_password=self._password) 430 else: 431 raise MissingDependencyError( 432 'Service account authentication requires PyOpenSSL. Please install ' 433 'this library and try again.') 434 435 436# TODO: oauth2client should expose _ServiceAccountCredentials as it is the only 437# way to properly set scopes. In the longer term this class should probably 438# be refactored into oauth2client directly in a way that allows for setting of 439# user agent and scopes. https://github.com/google/oauth2client/issues/164 440# pylint: disable=protected-access 441class ServiceAccountCredentials(service_account._ServiceAccountCredentials): 442 443 def to_json(self): 444 self.service_account_name = self._service_account_email 445 strip = (['_private_key'] + 446 Credentials.NON_SERIALIZED_MEMBERS) 447 return super(ServiceAccountCredentials, self)._to_json(strip) 448 449 @classmethod 450 def from_json(cls, s): 451 try: 452 data = json.loads(s) 453 retval = ServiceAccountCredentials( 454 service_account_id=data['_service_account_id'], 455 service_account_email=data['_service_account_email'], 456 private_key_id=data['_private_key_id'], 457 private_key_pkcs8_text=data['_private_key_pkcs8_text'], 458 scopes=[DEFAULT_SCOPE]) 459 # TODO: Need to define user agent here, 460 # but it is not known until runtime. 461 retval.invalid = data['invalid'] 462 retval.access_token = data['access_token'] 463 if 'token_expiry' in data: 464 retval.token_expiry = datetime.datetime.strptime( 465 data['token_expiry'], EXPIRY_FORMAT) 466 return retval 467 except KeyError, e: 468 raise Exception('Your JSON credentials are invalid; ' 469 'missing required entry %s.' % e[0]) 470# pylint: enable=protected-access 471 472 473class OAuth2JsonServiceAccountClient(_BaseOAuth2ServiceAccountClient): 474 """An OAuth2 service account client using .json keys.""" 475 476 def __init__(self, client_id, service_account_email, private_key_id, 477 private_key_pkcs8_text, access_token_cache=None, auth_uri=None, 478 token_uri=None, datetime_strategy=datetime.datetime, 479 disable_ssl_certificate_validation=False, 480 proxy_host=None, proxy_port=None, proxy_user=None, 481 proxy_pass=None, ca_certs_file=None): 482 # Avoid long repeated kwargs list. 483 # pylint: disable=g-doc-args 484 """Creates an OAuth2JsonServiceAccountClient. 485 486 Args: 487 client_id: The OAuth2 client ID of this client. 488 client_email: The email associated with this client. 489 private_key_id: The private key id associated with this service account. 490 private_key_pkcs8_text: The pkcs8 text containing the private key data. 491 492 Keyword arguments match the _BaseOAuth2ServiceAccountClient class. 493 """ 494 # pylint: enable=g-doc-args 495 super(OAuth2JsonServiceAccountClient, self).__init__( 496 client_id, auth_uri=auth_uri, token_uri=token_uri, 497 access_token_cache=access_token_cache, 498 datetime_strategy=datetime_strategy, 499 disable_ssl_certificate_validation=disable_ssl_certificate_validation, 500 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user, 501 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file) 502 self._service_account_email = service_account_email 503 self._private_key_id = private_key_id 504 self._private_key_pkcs8_text = private_key_pkcs8_text 505 506 def GetCredentials(self): 507 return ServiceAccountCredentials( 508 service_account_id=self._client_id, 509 service_account_email=self._service_account_email, 510 private_key_id=self._private_key_id, 511 private_key_pkcs8_text=self._private_key_pkcs8_text, 512 scopes=[DEFAULT_SCOPE]) 513 # TODO: Need to plumb user agent through here. 514 515 516class GsAccessTokenRefreshError(Exception): 517 """Transient error when requesting access token.""" 518 def __init__(self, e): 519 super(Exception, self).__init__(e) 520 521 522class GsInvalidRefreshTokenError(Exception): 523 def __init__(self, e): 524 super(Exception, self).__init__(e) 525 526 527class MissingDependencyError(Exception): 528 def __init__(self, e): 529 super(Exception, self).__init__(e) 530 531 532class OAuth2UserAccountClient(OAuth2Client): 533 """An OAuth2 client.""" 534 535 def __init__(self, token_uri, client_id, client_secret, refresh_token, 536 auth_uri=None, access_token_cache=None, 537 datetime_strategy=datetime.datetime, 538 disable_ssl_certificate_validation=False, 539 proxy_host=None, proxy_port=None, proxy_user=None, 540 proxy_pass=None, ca_certs_file=None): 541 """Creates an OAuth2UserAccountClient. 542 543 Args: 544 token_uri: The URI used to refresh access tokens. 545 client_id: The OAuth2 client ID of this client. 546 client_secret: The OAuth2 client secret of this client. 547 refresh_token: The token used to refresh the access token. 548 auth_uri: The URI for OAuth2 authorization. 549 access_token_cache: An optional instance of a TokenCache. If omitted or 550 None, an InMemoryTokenCache is used. 551 datetime_strategy: datetime module strategy to use. 552 disable_ssl_certificate_validation: True if certifications should not be 553 validated. 554 proxy_host: An optional string specifying the host name of an HTTP proxy 555 to be used. 556 proxy_port: An optional int specifying the port number of an HTTP proxy 557 to be used. 558 proxy_user: An optional string specifying the user name for interacting 559 with the HTTP proxy. 560 proxy_pass: An optional string specifying the password for interacting 561 with the HTTP proxy. 562 ca_certs_file: The cacerts.txt file to use. 563 """ 564 super(OAuth2UserAccountClient, self).__init__( 565 cache_key_base=refresh_token, auth_uri=auth_uri, token_uri=token_uri, 566 access_token_cache=access_token_cache, 567 datetime_strategy=datetime_strategy, 568 disable_ssl_certificate_validation=disable_ssl_certificate_validation, 569 proxy_host=proxy_host, proxy_port=proxy_port, proxy_user=proxy_user, 570 proxy_pass=proxy_pass, ca_certs_file=ca_certs_file) 571 self.token_uri = token_uri 572 self.client_id = client_id 573 self.client_secret = client_secret 574 self.refresh_token = refresh_token 575 576 def GetCredentials(self): 577 """Fetches a credentials objects from the provider's token endpoint.""" 578 access_token = self.GetAccessToken() 579 credentials = OAuth2Credentials( 580 access_token.token, self.client_id, self.client_secret, 581 self.refresh_token, access_token.expiry, self.token_uri, None) 582 return credentials 583 584 @Retry(GsAccessTokenRefreshError, 585 tries=config.get('OAuth2', 'oauth2_refresh_retries', 6), 586 timeout_secs=1) 587 def FetchAccessToken(self): 588 """Fetches an access token from the provider's token endpoint. 589 590 Fetches an access token from this client's OAuth2 provider's token endpoint. 591 592 Returns: 593 The fetched AccessToken. 594 """ 595 try: 596 http = self.CreateHttpRequest() 597 credentials = OAuth2Credentials(None, self.client_id, self.client_secret, 598 self.refresh_token, None, self.token_uri, None) 599 credentials.refresh(http) 600 return AccessToken(credentials.access_token, 601 credentials.token_expiry, datetime_strategy=self.datetime_strategy) 602 except AccessTokenRefreshError, e: 603 if 'Invalid response 403' in e.message: 604 # This is the most we can do at the moment to accurately detect rate 605 # limiting errors since they come back as 403s with no further 606 # information. 607 raise GsAccessTokenRefreshError(e) 608 elif 'invalid_grant' in e.message: 609 LOG.info(""" 610Attempted to retrieve an access token from an invalid refresh token. Two common 611cases in which you will see this error are: 6121. Your refresh token was revoked. 6132. Your refresh token was typed incorrectly. 614""") 615 raise GsInvalidRefreshTokenError(e) 616 else: 617 raise 618 619 620class OAuth2GCEClient(OAuth2Client): 621 """OAuth2 client for GCE instance.""" 622 623 def __init__(self): 624 super(OAuth2GCEClient, self).__init__( 625 cache_key_base='', 626 # Only InMemoryTokenCache can be used with empty cache_key_base. 627 access_token_cache=InMemoryTokenCache()) 628 629 @Retry(GsAccessTokenRefreshError, 630 tries=6, 631 timeout_secs=1) 632 def FetchAccessToken(self): 633 response = None 634 try: 635 http = httplib2.Http() 636 response, content = http.request(META_TOKEN_URI, method='GET', 637 body=None, headers=META_HEADERS) 638 except Exception: 639 raise GsAccessTokenRefreshError() 640 641 if response.status == 200: 642 d = json.loads(content) 643 644 return AccessToken( 645 d['access_token'], 646 datetime.datetime.now() + 647 datetime.timedelta(seconds=d.get('expires_in', 0)), 648 datetime_strategy=self.datetime_strategy) 649 650 651def _IsGCE(): 652 try: 653 http = httplib2.Http() 654 response, _ = http.request(METADATA_SERVER) 655 return response.status == 200 656 657 except (httplib2.ServerNotFoundError, socket.error): 658 # We might see something like "No route to host" propagated as a socket 659 # error. We might also catch transient socket errors, but at that point 660 # we're going to fail anyway, just with a different error message. With 661 # this approach, we'll avoid having to enumerate all possible non-transient 662 # socket errors. 663 return False 664 except Exception, e: 665 LOG.warning("Failed to determine whether we're running on GCE, so we'll" 666 "assume that we aren't: %s", e) 667 return False 668 669 return False 670 671 672def CreateOAuth2GCEClient(): 673 return OAuth2GCEClient() if _IsGCE() else None 674 675 676class AccessToken(object): 677 """Encapsulates an OAuth2 access token.""" 678 679 def __init__(self, token, expiry, datetime_strategy=datetime.datetime): 680 self.token = token 681 self.expiry = expiry 682 self.datetime_strategy = datetime_strategy 683 684 @staticmethod 685 def UnSerialize(query): 686 """Creates an AccessToken object from its serialized form.""" 687 688 def GetValue(d, key): 689 return (d.get(key, [None]))[0] 690 kv = cgi.parse_qs(query) 691 if not kv['token']: 692 return None 693 expiry = None 694 expiry_tuple = GetValue(kv, 'expiry') 695 if expiry_tuple: 696 try: 697 expiry = datetime.datetime( 698 *[int(n) for n in expiry_tuple.split(',')]) 699 except: 700 return None 701 return AccessToken(GetValue(kv, 'token'), expiry) 702 703 def Serialize(self): 704 """Serializes this object as URI-encoded key-value pairs.""" 705 # There's got to be a better way to serialize a datetime. Unfortunately, 706 # there is no reliable way to convert into a unix epoch. 707 kv = {'token': self.token} 708 if self.expiry: 709 t = self.expiry 710 tupl = (t.year, t.month, t.day, t.hour, t.minute, t.second, t.microsecond) 711 kv['expiry'] = ','.join([str(i) for i in tupl]) 712 return urllib.urlencode(kv) 713 714 def ShouldRefresh(self, time_delta=300): 715 """Whether the access token needs to be refreshed. 716 717 Args: 718 time_delta: refresh access token when it expires within time_delta secs. 719 720 Returns: 721 True if the token is expired or about to expire, False if the 722 token should be expected to work. Note that the token may still 723 be rejected, e.g. if it has been revoked server-side. 724 """ 725 if self.expiry is None: 726 return False 727 return (self.datetime_strategy.utcnow() 728 + datetime.timedelta(seconds=time_delta) > self.expiry) 729 730 def __eq__(self, other): 731 return self.token == other.token and self.expiry == other.expiry 732 733 def __ne__(self, other): 734 return not self.__eq__(other) 735 736 def __str__(self): 737 return 'AccessToken(token=%s, expiry=%sZ)' % (self.token, self.expiry) 738