• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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