home *** CD-ROM | disk | FTP | other *** search
/ Clickx 115 / Clickx 115.iso / software / tools / windows / tails-i386-0.16.iso / live / filesystem.squashfs / usr / share / pyshared / gdata / gauth.py < prev    next >
Encoding:
Python Source  |  2010-02-18  |  48.3 KB  |  1,307 lines

  1. #!/usr/bin/env python
  2. #
  3. # Copyright (C) 2009 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. # This module is used for version 2 of the Google Data APIs.
  19.  
  20.  
  21. """Provides auth related token classes and functions for Google Data APIs.
  22.  
  23. Token classes represent a user's authorization of this app to access their
  24. data. Usually these are not created directly but by a GDClient object.
  25.  
  26. ClientLoginToken
  27. AuthSubToken
  28. SecureAuthSubToken
  29. OAuthHmacToken
  30. OAuthRsaToken
  31. TwoLeggedOAuthHmacToken
  32. TwoLeggedOAuthRsaToken
  33.  
  34. Functions which are often used in application code (as opposed to just within
  35. the gdata-python-client library) are the following:
  36.  
  37. generate_auth_sub_url
  38. authorize_request_token
  39.  
  40. The following are helper functions which are used to save and load auth token
  41. objects in the App Engine datastore. These should only be used if you are using
  42. this library within App Engine:
  43.  
  44. ae_load
  45. ae_save
  46. """
  47.  
  48.  
  49. import time
  50. import random
  51. import urllib
  52. import atom.http_core
  53.  
  54.  
  55. __author__ = 'j.s@google.com (Jeff Scudder)'
  56.  
  57.  
  58. PROGRAMMATIC_AUTH_LABEL = 'GoogleLogin auth='
  59. AUTHSUB_AUTH_LABEL = 'AuthSub token='
  60.  
  61.  
  62. # This dict provides the AuthSub and OAuth scopes for all services by service
  63. # name. The service name (key) is used in ClientLogin requests.
  64. AUTH_SCOPES = {
  65.     'cl': ( # Google Calendar API
  66.         'https://www.google.com/calendar/feeds/',
  67.         'http://www.google.com/calendar/feeds/'),
  68.     'gbase': ( # Google Base API
  69.         'http://base.google.com/base/feeds/',
  70.         'http://www.google.com/base/feeds/'),
  71.     'blogger': ( # Blogger API
  72.         'http://www.blogger.com/feeds/',),
  73.     'codesearch': ( # Google Code Search API
  74.         'http://www.google.com/codesearch/feeds/',),
  75.     'cp': ( # Contacts API
  76.         'https://www.google.com/m8/feeds/',
  77.         'http://www.google.com/m8/feeds/'),
  78.     'finance': ( # Google Finance API
  79.         'http://finance.google.com/finance/feeds/',),
  80.     'health': ( # Google Health API
  81.         'https://www.google.com/health/feeds/',),
  82.     'writely': ( # Documents List API
  83.         'https://docs.google.com/feeds/',
  84.         'http://docs.google.com/feeds/'),
  85.     'lh2': ( # Picasa Web Albums API
  86.         'http://picasaweb.google.com/data/',),
  87.     'apps': ( # Google Apps Provisioning API
  88.         'http://www.google.com/a/feeds/',
  89.         'https://www.google.com/a/feeds/',
  90.         'http://apps-apis.google.com/a/feeds/',
  91.         'https://apps-apis.google.com/a/feeds/'),
  92.     'weaver': ( # Health H9 Sandbox
  93.         'https://www.google.com/h9/feeds/',),
  94.     'wise': ( # Spreadsheets Data API
  95.         'https://spreadsheets.google.com/feeds/',
  96.         'http://spreadsheets.google.com/feeds/'),
  97.     'sitemaps': ( # Google Webmaster Tools API
  98.         'https://www.google.com/webmasters/tools/feeds/',),
  99.     'youtube': ( # YouTube API
  100.         'http://gdata.youtube.com/feeds/api/',
  101.         'http://uploads.gdata.youtube.com/feeds/api',
  102.         'http://gdata.youtube.com/action/GetUploadToken'),
  103.     'books': ( # Google Books API
  104.         'http://www.google.com/books/feeds/',),
  105.     'analytics': ( # Google Analytics API
  106.         'https://www.google.com/analytics/feeds/',),
  107.     'jotspot': ( # Google Sites API
  108.         'http://sites.google.com/feeds/',
  109.         'https://sites.google.com/feeds/'),
  110.     'local': ( # Google Maps Data API
  111.         'http://maps.google.com/maps/feeds/',),
  112.     'code': ( # Project Hosting Data API
  113.         'http://code.google.com/feeds/issues',)}
  114.  
  115.  
  116.  
  117. class Error(Exception):
  118.   pass
  119.  
  120.  
  121. class UnsupportedTokenType(Error):
  122.   """Raised when token to or from blob is unable to convert the token."""
  123.   pass
  124.  
  125.  
  126. # ClientLogin functions and classes.
  127. def generate_client_login_request_body(email, password, service, source,
  128.     account_type='HOSTED_OR_GOOGLE', captcha_token=None,
  129.     captcha_response=None):
  130.   """Creates the body of the autentication request
  131.  
  132.   See http://code.google.com/apis/accounts/AuthForInstalledApps.html#Request
  133.   for more details.
  134.  
  135.   Args:
  136.     email: str
  137.     password: str
  138.     service: str
  139.     source: str
  140.     account_type: str (optional) Defaul is 'HOSTED_OR_GOOGLE', other valid
  141.         values are 'GOOGLE' and 'HOSTED'
  142.     captcha_token: str (optional)
  143.     captcha_response: str (optional)
  144.  
  145.   Returns:
  146.     The HTTP body to send in a request for a client login token.
  147.   """
  148.   # Create a POST body containing the user's credentials.
  149.   request_fields = {'Email': email,
  150.                     'Passwd': password,
  151.                     'accountType': account_type,
  152.                     'service': service,
  153.                     'source': source}
  154.   if captcha_token and captcha_response:
  155.     # Send the captcha token and response as part of the POST body if the
  156.     # user is responding to a captch challenge.
  157.     request_fields['logintoken'] = captcha_token
  158.     request_fields['logincaptcha'] = captcha_response
  159.   return urllib.urlencode(request_fields)
  160.  
  161.  
  162. GenerateClientLoginRequestBody = generate_client_login_request_body
  163.  
  164.  
  165. def get_client_login_token_string(http_body):
  166.   """Returns the token value for a ClientLoginToken.
  167.  
  168.   Reads the token from the server's response to a Client Login request and
  169.   creates the token value string to use in requests.
  170.  
  171.   Args:
  172.     http_body: str The body of the server's HTTP response to a Client Login
  173.         request
  174.  
  175.   Returns:
  176.     The token value string for a ClientLoginToken.
  177.   """
  178.   for response_line in http_body.splitlines():
  179.     if response_line.startswith('Auth='):
  180.       # Strip off the leading Auth= and return the Authorization value.
  181.       return response_line[5:]
  182.   return None
  183.  
  184.  
  185. GetClientLoginTokenString = get_client_login_token_string
  186.  
  187.  
  188. def get_captcha_challenge(http_body,
  189.     captcha_base_url='http://www.google.com/accounts/'):
  190.   """Returns the URL and token for a CAPTCHA challenge issued by the server.
  191.  
  192.   Args:
  193.     http_body: str The body of the HTTP response from the server which
  194.         contains the CAPTCHA challenge.
  195.     captcha_base_url: str This function returns a full URL for viewing the
  196.         challenge image which is built from the server's response. This
  197.         base_url is used as the beginning of the URL because the server
  198.         only provides the end of the URL. For example the server provides
  199.         'Captcha?ctoken=Hi...N' and the URL for the image is
  200.         'http://www.google.com/accounts/Captcha?ctoken=Hi...N'
  201.  
  202.   Returns:
  203.     A dictionary containing the information needed to repond to the CAPTCHA
  204.     challenge, the image URL and the ID token of the challenge. The
  205.     dictionary is in the form:
  206.     {'token': string identifying the CAPTCHA image,
  207.      'url': string containing the URL of the image}
  208.     Returns None if there was no CAPTCHA challenge in the response.
  209.   """
  210.   contains_captcha_challenge = False
  211.   captcha_parameters = {}
  212.   for response_line in http_body.splitlines():
  213.     if response_line.startswith('Error=CaptchaRequired'):
  214.       contains_captcha_challenge = True
  215.     elif response_line.startswith('CaptchaToken='):
  216.       # Strip off the leading CaptchaToken=
  217.       captcha_parameters['token'] = response_line[13:]
  218.     elif response_line.startswith('CaptchaUrl='):
  219.       captcha_parameters['url'] = '%s%s' % (captcha_base_url,
  220.           response_line[11:])
  221.   if contains_captcha_challenge:
  222.     return captcha_parameters
  223.   else:
  224.     return None
  225.  
  226.  
  227. GetCaptchaChallenge = get_captcha_challenge
  228.  
  229.  
  230. class ClientLoginToken(object):
  231.  
  232.   def __init__(self, token_string):
  233.     self.token_string = token_string
  234.  
  235.   def modify_request(self, http_request):
  236.     http_request.headers['Authorization'] = '%s%s' % (PROGRAMMATIC_AUTH_LABEL,
  237.         self.token_string)
  238.  
  239.   ModifyRequest = modify_request
  240.  
  241.  
  242. # AuthSub functions and classes.
  243. def _to_uri(str_or_uri):
  244.   if isinstance(str_or_uri, (str, unicode)):
  245.     return atom.http_core.Uri.parse_uri(str_or_uri)
  246.   return str_or_uri
  247.  
  248.  
  249. def generate_auth_sub_url(next, scopes, secure=False, session=True,
  250.     request_url=atom.http_core.parse_uri(
  251.         'https://www.google.com/accounts/AuthSubRequest'),
  252.     domain='default', scopes_param_prefix='auth_sub_scopes'):
  253.   """Constructs a URI for requesting a multiscope AuthSub token.
  254.  
  255.   The generated token will contain a URL parameter to pass along the
  256.   requested scopes to the next URL. When the Google Accounts page
  257.   redirects the broswser to the 'next' URL, it appends the single use
  258.   AuthSub token value to the URL as a URL parameter with the key 'token'.
  259.   However, the information about which scopes were requested is not
  260.   included by Google Accounts. This method adds the scopes to the next
  261.   URL before making the request so that the redirect will be sent to
  262.   a page, and both the token value and the list of scopes for which the token
  263.   was requested.
  264.  
  265.   Args:
  266.     next: atom.http_core.Uri or string The URL user will be sent to after
  267.           authorizing this web application to access their data.
  268.     scopes: list containint strings or atom.http_core.Uri objects. The URLs
  269.             of the services to be accessed. Could also be a single string
  270.             or single atom.http_core.Uri for requesting just one scope.
  271.     secure: boolean (optional) Determines whether or not the issued token
  272.             is a secure token.
  273.     session: boolean (optional) Determines whether or not the issued token
  274.              can be upgraded to a session token.
  275.     request_url: atom.http_core.Uri or str The beginning of the request URL.
  276.                  This is normally
  277.                  'http://www.google.com/accounts/AuthSubRequest' or
  278.                  '/accounts/AuthSubRequest'
  279.     domain: The domain which the account is part of. This is used for Google
  280.             Apps accounts, the default value is 'default' which means that
  281.             the requested account is a Google Account (@gmail.com for
  282.             example)
  283.     scopes_param_prefix: str (optional) The requested scopes are added as a
  284.                          URL parameter to the next URL so that the page at
  285.                          the 'next' URL can extract the token value and the
  286.                          valid scopes from the URL. The key for the URL
  287.                          parameter defaults to 'auth_sub_scopes'
  288.  
  289.   Returns:
  290.     An atom.http_core.Uri which the user's browser should be directed to in
  291.     order to authorize this application to access their information.
  292.   """
  293.   if isinstance(next, (str, unicode)):
  294.     next = atom.http_core.Uri.parse_uri(next)
  295.   # If the user passed in a string instead of a list for scopes, convert to
  296.   # a single item tuple.
  297.   if isinstance(scopes, (str, unicode, atom.http_core.Uri)):
  298.     scopes = (scopes,)
  299.   scopes_string = ' '.join([str(scope) for scope in scopes])
  300.   next.query[scopes_param_prefix] = scopes_string
  301.  
  302.   if isinstance(request_url, (str, unicode)):
  303.     request_url = atom.http_core.Uri.parse_uri(request_url)
  304.   request_url.query['next'] = str(next)
  305.   request_url.query['scope'] = scopes_string
  306.   if session:
  307.     request_url.query['session'] = '1'
  308.   else:
  309.     request_url.query['session'] = '0'
  310.   if secure:
  311.     request_url.query['secure'] = '1'
  312.   else:
  313.     request_url.query['secure'] = '0'
  314.   request_url.query['hd'] = domain
  315.   return request_url
  316.  
  317.  
  318. def auth_sub_string_from_url(url, scopes_param_prefix='auth_sub_scopes'):
  319.   """Finds the token string (and scopes) after the browser is redirected.
  320.  
  321.   After the Google Accounts AuthSub pages redirect the user's broswer back to
  322.   the web application (using the 'next' URL from the request) the web app must
  323.   extract the token from the current page's URL. The token is provided as a
  324.   URL parameter named 'token' and if generate_auth_sub_url was used to create
  325.   the request, the token's valid scopes are included in a URL parameter whose
  326.   name is specified in scopes_param_prefix.
  327.  
  328.   Args:
  329.     url: atom.url.Url or str representing the current URL. The token value
  330.          and valid scopes should be included as URL parameters.
  331.     scopes_param_prefix: str (optional) The URL parameter key which maps to
  332.                          the list of valid scopes for the token.
  333.  
  334.   Returns:
  335.     A tuple containing the token value as a string, and a tuple of scopes
  336.     (as atom.http_core.Uri objects) which are URL prefixes under which this
  337.     token grants permission to read and write user data.
  338.     (token_string, (scope_uri, scope_uri, scope_uri, ...))
  339.     If no scopes were included in the URL, the second value in the tuple is
  340.     None. If there was no token param in the url, the tuple returned is
  341.     (None, None)
  342.   """
  343.   if isinstance(url, (str, unicode)):
  344.     url = atom.http_core.Uri.parse_uri(url)
  345.   if 'token' not in url.query:
  346.     return (None, None)
  347.   token = url.query['token']
  348.   # TODO: decide whether no scopes should be None or ().
  349.   scopes = None # Default to None for no scopes.
  350.   if scopes_param_prefix in url.query:
  351.     scopes = tuple(url.query[scopes_param_prefix].split(' '))
  352.   return (token, scopes)
  353.  
  354.  
  355. AuthSubStringFromUrl = auth_sub_string_from_url
  356.  
  357.  
  358. def auth_sub_string_from_body(http_body):
  359.   """Extracts the AuthSub token from an HTTP body string.
  360.  
  361.   Used to find the new session token after making a request to upgrade a
  362.   single use AuthSub token.
  363.  
  364.   Args:
  365.     http_body: str The repsonse from the server which contains the AuthSub
  366.         key. For example, this function would find the new session token
  367.         from the server's response to an upgrade token request.
  368.  
  369.   Returns:
  370.     The raw token value string to use in an AuthSubToken object.
  371.   """
  372.   for response_line in http_body.splitlines():
  373.     if response_line.startswith('Token='):
  374.       # Strip off Token= and return the token value string.
  375.       return response_line[6:]
  376.   return None
  377.  
  378.  
  379. class AuthSubToken(object):
  380.  
  381.   def __init__(self, token_string, scopes=None):
  382.     self.token_string = token_string
  383.     self.scopes = scopes or []
  384.  
  385.   def modify_request(self, http_request):
  386.     """Sets Authorization header, allows app to act on the user's behalf."""
  387.     http_request.headers['Authorization'] = '%s%s' % (AUTHSUB_AUTH_LABEL,
  388.         self.token_string)
  389.  
  390.   ModifyRequest = modify_request
  391.  
  392.   def from_url(str_or_uri):
  393.     """Creates a new AuthSubToken using information in the URL.
  394.  
  395.     Uses auth_sub_string_from_url.
  396.  
  397.     Args:
  398.       str_or_uri: The current page's URL (as a str or atom.http_core.Uri)
  399.                   which should contain a token query parameter since the
  400.                   Google auth server redirected the user's browser to this
  401.                   URL.
  402.     """
  403.     token_and_scopes = auth_sub_string_from_url(str_or_uri)
  404.     return AuthSubToken(token_and_scopes[0], token_and_scopes[1])
  405.  
  406.   from_url = staticmethod(from_url)
  407.   FromUrl = from_url
  408.  
  409.   def _upgrade_token(self, http_body):
  410.     """Replaces the token value with a session token from the auth server.
  411.  
  412.     Uses the response of a token upgrade request to modify this token. Uses
  413.     auth_sub_string_from_body.
  414.     """
  415.     self.token_string = auth_sub_string_from_body(http_body)
  416.  
  417.  
  418. # Functions and classes for Secure-mode AuthSub
  419. def build_auth_sub_data(http_request, timestamp, nonce):
  420.   """Creates the data string which must be RSA-signed in secure requests.
  421.  
  422.   For more details see the documenation on secure AuthSub requests:
  423.   http://code.google.com/apis/accounts/docs/AuthSub.html#signingrequests
  424.  
  425.   Args:
  426.     http_request: The request being made to the server. The Request's URL
  427.         must be complete before this signature is calculated as any changes
  428.         to the URL will invalidate the signature.
  429.     nonce: str Random 64-bit, unsigned number encoded as an ASCII string in
  430.         decimal format. The nonce/timestamp pair should always be unique to
  431.         prevent replay attacks.
  432.     timestamp: Integer representing the time the request is sent. The
  433.         timestamp should be expressed in number of seconds after January 1,
  434.         1970 00:00:00 GMT.
  435.   """
  436.   return '%s %s %s %s' % (http_request.method, str(http_request.uri),
  437.                           str(timestamp), nonce)
  438.  
  439.  
  440. def generate_signature(data, rsa_key):
  441.   """Signs the data string for a secure AuthSub request."""
  442.   import base64
  443.   try:
  444.     from tlslite.utils import keyfactory
  445.   except ImportError:
  446.     from gdata.tlslite.utils import keyfactory
  447.   private_key = keyfactory.parsePrivateKey(rsa_key)
  448.   signed = private_key.hashAndSign(data)
  449.   # Python2.3 and lower does not have the base64.b64encode function.
  450.   if hasattr(base64, 'b64encode'):
  451.     return base64.b64encode(signed)
  452.   else:
  453.     return base64.encodestring(signed).replace('\n', '')
  454.  
  455.  
  456. class SecureAuthSubToken(AuthSubToken):
  457.  
  458.   def __init__(self, token_string, rsa_private_key, scopes=None):
  459.     self.token_string = token_string
  460.     self.scopes = scopes or []
  461.     self.rsa_private_key = rsa_private_key
  462.  
  463.   def from_url(str_or_uri, rsa_private_key):
  464.     """Creates a new SecureAuthSubToken using information in the URL.
  465.  
  466.     Uses auth_sub_string_from_url.
  467.  
  468.     Args:
  469.       str_or_uri: The current page's URL (as a str or atom.http_core.Uri)
  470.           which should contain a token query parameter since the Google auth
  471.           server redirected the user's browser to this URL.
  472.       rsa_private_key: str the private RSA key cert used to sign all requests
  473.           made with this token.
  474.     """
  475.     token_and_scopes = auth_sub_string_from_url(str_or_uri)
  476.     return SecureAuthSubToken(token_and_scopes[0], rsa_private_key,
  477.                               token_and_scopes[1])
  478.  
  479.   from_url = staticmethod(from_url)
  480.   FromUrl = from_url
  481.  
  482.   def modify_request(self, http_request):
  483.     """Sets the Authorization header and includes a digital signature.
  484.  
  485.     Calculates a digital signature using the private RSA key, a timestamp
  486.     (uses now at the time this method is called) and a random nonce.
  487.  
  488.     Args:
  489.       http_request: The atom.http_core.HttpRequest which contains all of the
  490.           information needed to send a request to the remote server. The
  491.           URL and the method of the request must be already set and cannot be
  492.           changed after this token signs the request, or the signature will
  493.           not be valid.
  494.     """
  495.     timestamp = str(int(time.time()))
  496.     nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
  497.     data = build_auth_sub_data(http_request, timestamp, nonce)
  498.     signature = generate_signature(data, self.rsa_private_key)
  499.     http_request.headers['Authorization'] = (
  500.         '%s%s sigalg="rsa-sha1" data="%s" sig="%s"' % (AUTHSUB_AUTH_LABEL,
  501.             self.token_string, data, signature))
  502.  
  503.   ModifyRequest = modify_request
  504.  
  505.  
  506. # OAuth functions and classes.
  507. RSA_SHA1 = 'RSA-SHA1'
  508. HMAC_SHA1 = 'HMAC-SHA1'
  509.  
  510.  
  511. def build_oauth_base_string(http_request, consumer_key, nonce, signaure_type,
  512.                             timestamp, version, next='oob', token=None,
  513.                             verifier=None):
  514.   """Generates the base string to be signed in the OAuth request.
  515.  
  516.   Args:
  517.     http_request: The request being made to the server. The Request's URL
  518.         must be complete before this signature is calculated as any changes
  519.         to the URL will invalidate the signature.
  520.     consumer_key: Domain identifying the third-party web application. This is
  521.         the domain used when registering the application with Google. It
  522.         identifies who is making the request on behalf of the user.
  523.     nonce: Random 64-bit, unsigned number encoded as an ASCII string in decimal
  524.         format. The nonce/timestamp pair should always be unique to prevent
  525.         replay attacks.
  526.     signaure_type: either RSA_SHA1 or HMAC_SHA1
  527.     timestamp: Integer representing the time the request is sent. The
  528.         timestamp should be expressed in number of seconds after January 1,
  529.         1970 00:00:00 GMT.
  530.     version: The OAuth version used by the requesting web application. This
  531.         value must be '1.0' or '1.0a'. If not provided, Google assumes version
  532.         1.0 is in use.
  533.     next: The URL the user should be redirected to after granting access
  534.         to a Google service(s). It can include url-encoded query parameters.
  535.         The default value is 'oob'. (This is the oauth_callback.)
  536.     token: The string for the OAuth request token or OAuth access token.
  537.     verifier: str Sent as the oauth_verifier and required when upgrading a
  538.         request token to an access token.
  539.   """
  540.   # First we must build the canonical base string for the request.
  541.   params = http_request.uri.query.copy()
  542.   params['oauth_consumer_key'] = consumer_key
  543.   params['oauth_nonce'] = nonce
  544.   params['oauth_signature_method'] = signaure_type
  545.   params['oauth_timestamp'] = str(timestamp)
  546.   if next is not None:
  547.     params['oauth_callback'] = str(next)
  548.   if token is not None:
  549.     params['oauth_token'] = token
  550.   if version is not None:
  551.     params['oauth_version'] = version
  552.   if verifier is not None:
  553.     params['oauth_verifier'] = verifier
  554.   # We need to get the key value pairs in lexigraphically sorted order.
  555.   sorted_keys = None
  556.   try:
  557.     sorted_keys = sorted(params.keys())
  558.   # The sorted function is not available in Python2.3 and lower
  559.   except NameError:
  560.     sorted_keys = params.keys()
  561.     sorted_keys.sort()
  562.   pairs = []
  563.   for key in sorted_keys:
  564.     pairs.append('%s=%s' % (urllib.quote(key, safe='~'),
  565.                             urllib.quote(params[key], safe='~')))
  566.   # We want to escape /'s too, so use safe='~'
  567.   all_parameters = urllib.quote('&'.join(pairs), safe='~')
  568.   normailzed_host = http_request.uri.host.lower()
  569.   normalized_scheme = (http_request.uri.scheme or 'http').lower()
  570.   non_default_port = None
  571.   if (http_request.uri.port is not None
  572.       and ((normalized_scheme == 'https' and http_request.uri.port != 443)
  573.            or (normalized_scheme == 'http' and http_request.uri.port != 80))):
  574.     non_default_port = http_request.uri.port
  575.   path = http_request.uri.path or '/'
  576.   request_path = None
  577.   if not path.startswith('/'):
  578.     path = '/%s' % path
  579.   if non_default_port is not None:
  580.     # Set the only safe char in url encoding to ~ since we want to escape /
  581.     # as well.
  582.     request_path = urllib.quote('%s://%s:%s%s' % (
  583.         normalized_scheme, normailzed_host, non_default_port, path), safe='~')
  584.   else:
  585.     # Set the only safe char in url encoding to ~ since we want to escape /
  586.     # as well.
  587.     request_path = urllib.quote('%s://%s%s' % (
  588.         normalized_scheme, normailzed_host, path), safe='~')
  589.   # TODO: ensure that token escaping logic is correct, not sure if the token
  590.   # value should be double escaped instead of single.
  591.   base_string = '&'.join((http_request.method.upper(), request_path,
  592.                           all_parameters))
  593.   # Now we have the base string, we can calculate the oauth_signature.
  594.   return base_string
  595.  
  596.  
  597. def generate_hmac_signature(http_request, consumer_key, consumer_secret,
  598.                             timestamp, nonce, version, next='oob',
  599.                             token=None, token_secret=None, verifier=None):
  600.   import hmac
  601.   import base64
  602.   base_string = build_oauth_base_string(
  603.       http_request, consumer_key, nonce, HMAC_SHA1, timestamp, version,
  604.       next, token, verifier=verifier)
  605.   hash_key = None
  606.   hashed = None
  607.   if token_secret is not None:
  608.     hash_key = '%s&%s' % (urllib.quote(consumer_secret, safe='~'),
  609.                           urllib.quote(token_secret, safe='~'))
  610.   else:
  611.     hash_key = '%s&' % urllib.quote(consumer_secret, safe='~')
  612.   try:
  613.     import hashlib
  614.     hashed = hmac.new(hash_key, base_string, hashlib.sha1)
  615.   except ImportError:
  616.     import sha
  617.     hashed = hmac.new(hash_key, base_string, sha)
  618.   # Python2.3 does not have base64.b64encode.
  619.   if hasattr(base64, 'b64encode'):
  620.     return base64.b64encode(hashed.digest())
  621.   else:
  622.     return base64.encodestring(hashed.digest()).replace('\n', '')
  623.  
  624.  
  625. def generate_rsa_signature(http_request, consumer_key, rsa_key,
  626.                            timestamp, nonce, version, next='oob',
  627.                            token=None, token_secret=None, verifier=None):
  628.   import base64
  629.   try:
  630.     from tlslite.utils import keyfactory
  631.   except ImportError:
  632.     from gdata.tlslite.utils import keyfactory
  633.   base_string = build_oauth_base_string(
  634.       http_request, consumer_key, nonce, RSA_SHA1, timestamp, version,
  635.       next, token, verifier=verifier)
  636.   private_key = keyfactory.parsePrivateKey(rsa_key)
  637.   # Sign using the key
  638.   signed = private_key.hashAndSign(base_string)
  639.   # Python2.3 does not have base64.b64encode.
  640.   if hasattr(base64, 'b64encode'):
  641.     return base64.b64encode(signed)
  642.   else:
  643.     return base64.encodestring(signed).replace('\n', '')
  644.  
  645.  
  646. def generate_auth_header(consumer_key, timestamp, nonce, signature_type,
  647.                          signature, version='1.0', next=None, token=None,
  648.                          verifier=None):
  649.   """Builds the Authorization header to be sent in the request.
  650.  
  651.   Args:
  652.     consumer_key: Identifies the application making the request (str).
  653.     timestamp:
  654.     nonce:
  655.     signature_type: One of either HMAC_SHA1 or RSA_SHA1
  656.     signature: The HMAC or RSA signature for the request as a base64
  657.         encoded string.
  658.     version: The version of the OAuth protocol that this request is using.
  659.         Default is '1.0'
  660.     next: The URL of the page that the user's browser should be sent to
  661.         after they authorize the token. (Optional)
  662.     token: str The OAuth token value to be used in the oauth_token parameter
  663.         of the header.
  664.     verifier: str The OAuth verifier which must be included when you are
  665.         upgrading a request token to an access token.
  666.   """
  667.   params = {
  668.       'oauth_consumer_key': consumer_key,
  669.       'oauth_version': version,
  670.       'oauth_nonce': nonce,
  671.       'oauth_timestamp': str(timestamp),
  672.       'oauth_signature_method': signature_type,
  673.       'oauth_signature': signature}
  674.   if next is not None:
  675.     params['oauth_callback'] = str(next)
  676.   if token is not None:
  677.     params['oauth_token'] = token
  678.   if verifier is not None:
  679.     params['oauth_verifier'] = verifier
  680.   pairs = [
  681.       '%s="%s"' % (
  682.           k, urllib.quote(v, safe='~')) for k, v in params.iteritems()]
  683.   return 'OAuth %s' % (', '.join(pairs))
  684.  
  685.  
  686. REQUEST_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetRequestToken'
  687. ACCESS_TOKEN_URL = 'https://www.google.com/accounts/OAuthGetAccessToken'
  688.  
  689.  
  690. def generate_request_for_request_token(
  691.     consumer_key, signature_type, scopes, rsa_key=None, consumer_secret=None,
  692.     auth_server_url=REQUEST_TOKEN_URL, next='oob', version='1.0'):
  693.   """Creates request to be sent to auth server to get an OAuth request token.
  694.  
  695.   Args:
  696.     consumer_key:
  697.     signature_type: either RSA_SHA1 or HMAC_SHA1. The rsa_key must be
  698.         provided if the signature type is RSA but if the signature method
  699.         is HMAC, the consumer_secret must be used.
  700.     scopes: List of URL prefixes for the data which we want to access. For
  701.         example, to request access to the user's Blogger and Google Calendar
  702.         data, we would request
  703.         ['http://www.blogger.com/feeds/',
  704.          'https://www.google.com/calendar/feeds/',
  705.          'http://www.google.com/calendar/feeds/']
  706.     rsa_key: Only used if the signature method is RSA_SHA1.
  707.     consumer_secret: Only used if the signature method is HMAC_SHA1.
  708.     auth_server_url: The URL to which the token request should be directed.
  709.         Defaults to 'https://www.google.com/accounts/OAuthGetRequestToken'.
  710.     next: The URL of the page that the user's browser should be sent to
  711.         after they authorize the token. (Optional)
  712.     version: The OAuth version used by the requesting web application.
  713.         Defaults to '1.0a'
  714.  
  715.   Returns:
  716.     An atom.http_core.HttpRequest object with the URL, Authorization header
  717.     and body filled in.
  718.   """
  719.   request = atom.http_core.HttpRequest(auth_server_url, 'POST')
  720.   # Add the requested auth scopes to the Auth request URL.
  721.   if scopes:
  722.     request.uri.query['scope'] = ' '.join(scopes)
  723.  
  724.   timestamp = str(int(time.time()))
  725.   nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
  726.   signature = None
  727.   if signature_type == HMAC_SHA1:
  728.     signature = generate_hmac_signature(
  729.         request, consumer_key, consumer_secret, timestamp, nonce, version,
  730.         next=next)
  731.   elif signature_type == RSA_SHA1:
  732.     signature = generate_rsa_signature(
  733.         request, consumer_key, rsa_key, timestamp, nonce, version, next=next)
  734.   else:
  735.     return None
  736.  
  737.   request.headers['Authorization'] = generate_auth_header(
  738.       consumer_key, timestamp, nonce, signature_type, signature, version,
  739.       next)
  740.   request.headers['Content-Length'] = '0'
  741.   return request
  742.  
  743.  
  744. def generate_request_for_access_token(
  745.     request_token, auth_server_url=ACCESS_TOKEN_URL):
  746.   """Creates a request to ask the OAuth server for an access token.
  747.  
  748.   Requires a request token which the user has authorized. See the
  749.   documentation on OAuth with Google Data for more details:
  750.   http://code.google.com/apis/accounts/docs/OAuth.html#AccessToken
  751.  
  752.   Args:
  753.     request_token: An OAuthHmacToken or OAuthRsaToken which the user has
  754.         approved using their browser.
  755.     auth_server_url: (optional) The URL at which the OAuth access token is
  756.         requested. Defaults to
  757.         https://www.google.com/accounts/OAuthGetAccessToken
  758.  
  759.   Returns:
  760.     A new HttpRequest object which can be sent to the OAuth server to
  761.     request an OAuth Access Token.
  762.   """
  763.   http_request = atom.http_core.HttpRequest(auth_server_url, 'POST')
  764.   http_request.headers['Content-Length'] = '0'
  765.   return request_token.modify_request(http_request)
  766.  
  767.  
  768. def oauth_token_info_from_body(http_body):
  769.   """Exracts an OAuth request token from the server's response.
  770.  
  771.   Returns:
  772.     A tuple of strings containing the OAuth token and token secret. If
  773.     neither of these are present in the body, returns (None, None)
  774.   """
  775.   token = None
  776.   token_secret = None
  777.   for pair in http_body.split('&'):
  778.     if pair.startswith('oauth_token='):
  779.       token = urllib.unquote(pair[len('oauth_token='):])
  780.     if pair.startswith('oauth_token_secret='):
  781.       token_secret = urllib.unquote(pair[len('oauth_token_secret='):])
  782.   return (token, token_secret)
  783.  
  784.  
  785. def hmac_token_from_body(http_body, consumer_key, consumer_secret,
  786.                          auth_state):
  787.   token_value, token_secret = oauth_token_info_from_body(http_body)
  788.   token = OAuthHmacToken(consumer_key, consumer_secret, token_value,
  789.                          token_secret, auth_state)
  790.   return token
  791.  
  792.  
  793. def rsa_token_from_body(http_body, consumer_key, rsa_private_key,
  794.                         auth_state):
  795.   token_value, token_secret = oauth_token_info_from_body(http_body)
  796.   token = OAuthRsaToken(consumer_key, rsa_private_key, token_value,
  797.                         token_secret, auth_state)
  798.   return token
  799.  
  800.  
  801. DEFAULT_DOMAIN = 'default'
  802. OAUTH_AUTHORIZE_URL = 'https://www.google.com/accounts/OAuthAuthorizeToken'
  803.  
  804.  
  805. def generate_oauth_authorization_url(
  806.     token, next=None, hd=DEFAULT_DOMAIN, hl=None, btmpl=None,
  807.     auth_server=OAUTH_AUTHORIZE_URL):
  808.   """Creates a URL for the page where the request token can be authorized.
  809.  
  810.   Args:
  811.     token: str The request token from the OAuth server.
  812.     next: str (optional) URL the user should be redirected to after granting
  813.         access to a Google service(s). It can include url-encoded query
  814.         parameters.
  815.     hd: str (optional) Identifies a particular hosted domain account to be
  816.         accessed (for example, 'mycollege.edu'). Uses 'default' to specify a
  817.         regular Google account ('username@gmail.com').
  818.     hl: str (optional) An ISO 639 country code identifying what language the
  819.         approval page should be translated in (for example, 'hl=en' for
  820.         English). The default is the user's selected language.
  821.     btmpl: str (optional) Forces a mobile version of the approval page. The
  822.         only accepted value is 'mobile'.
  823.     auth_server: str (optional) The start of the token authorization web
  824.         page. Defaults to
  825.         'https://www.google.com/accounts/OAuthAuthorizeToken'
  826.  
  827.   Returns:
  828.     An atom.http_core.Uri pointing to the token authorization page where the
  829.     user may allow or deny this app to access their Google data.
  830.   """
  831.   uri = atom.http_core.Uri.parse_uri(auth_server)
  832.   uri.query['oauth_token'] = token
  833.   uri.query['hd'] = hd
  834.   if next is not None:
  835.     uri.query['oauth_callback'] = str(next)
  836.   if hl is not None:
  837.     uri.query['hl'] = hl
  838.   if btmpl is not None:
  839.     uri.query['btmpl'] = btmpl
  840.   return uri
  841.  
  842.  
  843. def oauth_token_info_from_url(url):
  844.   """Exracts an OAuth access token from the redirected page's URL.
  845.  
  846.   Returns:
  847.     A tuple of strings containing the OAuth token and the OAuth verifier which
  848.     need to sent when upgrading a request token to an access token.
  849.   """
  850.   if isinstance(url, (str, unicode)):
  851.     url = atom.http_core.Uri.parse_uri(url)
  852.   token = None
  853.   verifier = None
  854.   if 'oauth_token' in url.query:
  855.     token = urllib.unquote(url.query['oauth_token'])
  856.   if 'oauth_verifier' in url.query:
  857.     verifier = urllib.unquote(url.query['oauth_verifier'])
  858.   return (token, verifier)
  859.  
  860.  
  861. def authorize_request_token(request_token, url):
  862.   """Adds information to request token to allow it to become an access token.
  863.  
  864.   Modifies the request_token object passed in by setting and unsetting the
  865.   necessary fields to allow this token to form a valid upgrade request.
  866.  
  867.   Args:
  868.     request_token: The OAuth request token which has been authorized by the
  869.         user. In order for this token to be upgraded to an access token,
  870.         certain fields must be extracted from the URL and added to the token
  871.         so that they can be passed in an upgrade-token request.
  872.     url: The URL of the current page which the user's browser was redirected
  873.         to after they authorized access for the app. This function extracts
  874.         information from the URL which is needed to upgraded the token from
  875.         a request token to an access token.
  876.  
  877.   Returns:
  878.     The same token object which was passed in.
  879.   """
  880.   token, verifier = oauth_token_info_from_url(url)
  881.   request_token.token = token
  882.   request_token.verifier = verifier
  883.   request_token.auth_state = AUTHORIZED_REQUEST_TOKEN
  884.   return request_token
  885.  
  886.  
  887. AuthorizeRequestToken = authorize_request_token
  888.  
  889.  
  890. def upgrade_to_access_token(request_token, server_response_body):
  891.   """Extracts access token information from response to an upgrade request.
  892.  
  893.   Once the server has responded with the new token info for the OAuth
  894.   access token, this method modifies the request_token to set and unset
  895.   necessary fields to create valid OAuth authorization headers for requests.
  896.  
  897.   Args:
  898.     request_token: An OAuth token which this function modifies to allow it
  899.         to be used as an access token.
  900.     server_response_body: str The server's response to an OAuthAuthorizeToken
  901.         request. This should contain the new token and token_secret which
  902.         are used to generate the signature and parameters of the Authorization
  903.         header in subsequent requests to Google Data APIs.
  904.  
  905.   Returns:
  906.     The same token object which was passed in.
  907.   """
  908.   token, token_secret = oauth_token_info_from_body(server_response_body)
  909.   request_token.token = token
  910.   request_token.token_secret = token_secret
  911.   request_token.auth_state = ACCESS_TOKEN
  912.   request_token.next = None
  913.   request_token.verifier = None
  914.   return request_token
  915.  
  916.  
  917. UpgradeToAccessToken = upgrade_to_access_token
  918.  
  919.  
  920. REQUEST_TOKEN = 1
  921. AUTHORIZED_REQUEST_TOKEN = 2
  922. ACCESS_TOKEN = 3
  923.  
  924.  
  925. class OAuthHmacToken(object):
  926.   SIGNATURE_METHOD = HMAC_SHA1
  927.  
  928.   def __init__(self, consumer_key, consumer_secret, token, token_secret,
  929.                auth_state, next=None, verifier=None):
  930.     self.consumer_key = consumer_key
  931.     self.consumer_secret = consumer_secret
  932.     self.token = token
  933.     self.token_secret = token_secret
  934.     self.auth_state = auth_state
  935.     self.next = next
  936.     self.verifier = verifier # Used to convert request token to access token.
  937.  
  938.   def generate_authorization_url(
  939.       self, google_apps_domain=DEFAULT_DOMAIN, language=None, btmpl=None,
  940.       auth_server=OAUTH_AUTHORIZE_URL):
  941.     """Creates the URL at which the user can authorize this app to access.
  942.  
  943.     Args:
  944.       google_apps_domain: str (optional) If the user should be signing in
  945.           using an account under a known Google Apps domain, provide the
  946.           domain name ('example.com') here. If not provided, 'default'
  947.           will be used, and the user will be prompted to select an account
  948.           if they are signed in with a Google Account and Google Apps
  949.           accounts.
  950.       language: str (optional) An ISO 639 country code identifying what
  951.           language the approval page should be translated in (for example,
  952.           'en' for English). The default is the user's selected language.
  953.       btmpl: str (optional) Forces a mobile version of the approval page. The
  954.         only accepted value is 'mobile'.
  955.       auth_server: str (optional) The start of the token authorization web
  956.         page. Defaults to
  957.         'https://www.google.com/accounts/OAuthAuthorizeToken'
  958.     """
  959.     return generate_oauth_authorization_url(
  960.         self.token, hd=google_apps_domain, hl=language, btmpl=btmpl,
  961.         auth_server=auth_server)
  962.  
  963.   GenerateAuthorizationUrl = generate_authorization_url
  964.  
  965.   def modify_request(self, http_request):
  966.     """Sets the Authorization header in the HTTP request using the token.
  967.  
  968.     Calculates an HMAC signature using the information in the token to
  969.     indicate that the request came from this application and that this
  970.     application has permission to access a particular user's data.
  971.  
  972.     Returns:
  973.       The same HTTP request object which was passed in.
  974.     """
  975.     timestamp = str(int(time.time()))
  976.     nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
  977.     signature = generate_hmac_signature(
  978.         http_request, self.consumer_key, self.consumer_secret, timestamp,
  979.         nonce, version='1.0', next=self.next, token=self.token,
  980.         token_secret=self.token_secret, verifier=self.verifier)
  981.     http_request.headers['Authorization'] = generate_auth_header(
  982.         self.consumer_key, timestamp, nonce, HMAC_SHA1, signature,
  983.         version='1.0', next=self.next, token=self.token,
  984.         verifier=self.verifier)
  985.     return http_request
  986.  
  987.   ModifyRequest = modify_request
  988.  
  989.  
  990. class OAuthRsaToken(OAuthHmacToken):
  991.   SIGNATURE_METHOD = RSA_SHA1
  992.  
  993.   def __init__(self, consumer_key, rsa_private_key, token, token_secret,
  994.                auth_state, next=None, verifier=None):
  995.     self.consumer_key = consumer_key
  996.     self.rsa_private_key = rsa_private_key
  997.     self.token = token
  998.     self.token_secret = token_secret
  999.     self.auth_state = auth_state
  1000.     self.next = next
  1001.     self.verifier = verifier # Used to convert request token to access token.
  1002.  
  1003.   def modify_request(self, http_request):
  1004.     """Sets the Authorization header in the HTTP request using the token.
  1005.  
  1006.     Calculates an RSA signature using the information in the token to
  1007.     indicate that the request came from this application and that this
  1008.     application has permission to access a particular user's data.
  1009.  
  1010.     Returns:
  1011.       The same HTTP request object which was passed in.
  1012.     """
  1013.     timestamp = str(int(time.time()))
  1014.     nonce = ''.join([str(random.randint(0, 9)) for i in xrange(15)])
  1015.     signature = generate_rsa_signature(
  1016.         http_request, self.consumer_key, self.rsa_private_key, timestamp,
  1017.         nonce, version='1.0', next=self.next, token=self.token,
  1018.         token_secret=self.token_secret, verifier=self.verifier)
  1019.     http_request.headers['Authorization'] = generate_auth_header(
  1020.         self.consumer_key, timestamp, nonce, RSA_SHA1, signature,
  1021.         version='1.0', next=self.next, token=self.token,
  1022.         verifier=self.verifier)
  1023.     return http_request
  1024.  
  1025.   ModifyRequest = modify_request
  1026.  
  1027.  
  1028. class TwoLeggedOAuthHmacToken(OAuthHmacToken):
  1029.  
  1030.   def __init__(self, consumer_key, consumer_secret, requestor_id):
  1031.     self.requestor_id = requestor_id
  1032.     OAuthHmacToken.__init__(
  1033.         self, consumer_key, consumer_secret, None, None, ACCESS_TOKEN,
  1034.         next=None, verifier=None)
  1035.  
  1036.   def modify_request(self, http_request):
  1037.     """Sets the Authorization header in the HTTP request using the token.
  1038.  
  1039.     Calculates an HMAC signature using the information in the token to
  1040.     indicate that the request came from this application and that this
  1041.     application has permission to access a particular user's data using 2LO.
  1042.  
  1043.     Returns:
  1044.       The same HTTP request object which was passed in.
  1045.     """
  1046.     http_request.uri.query['xoauth_requestor_id'] = self.requestor_id
  1047.     return OAuthHmacToken.modify_request(self, http_request)
  1048.  
  1049.   ModifyRequest = modify_request
  1050.  
  1051.  
  1052. class TwoLeggedOAuthRsaToken(OAuthRsaToken):
  1053.  
  1054.   def __init__(self, consumer_key, rsa_private_key, requestor_id):
  1055.     self.requestor_id = requestor_id
  1056.     OAuthRsaToken.__init__(
  1057.         self, consumer_key, rsa_private_key, None, None, ACCESS_TOKEN,
  1058.         next=None, verifier=None)
  1059.  
  1060.   def modify_request(self, http_request):
  1061.     """Sets the Authorization header in the HTTP request using the token.
  1062.  
  1063.     Calculates an RSA signature using the information in the token to
  1064.     indicate that the request came from this application and that this
  1065.     application has permission to access a particular user's data using 2LO.
  1066.  
  1067.     Returns:
  1068.       The same HTTP request object which was passed in.
  1069.     """
  1070.     http_request.uri.query['xoauth_requestor_id'] = self.requestor_id
  1071.     return OAuthRsaToken.modify_request(self, http_request)
  1072.  
  1073.   ModifyRequest = modify_request
  1074.  
  1075.  
  1076. def _join_token_parts(*args):
  1077.   """"Escapes and combines all strings passed in.
  1078.  
  1079.   Used to convert a token object's members into a string instead of
  1080.   using pickle.
  1081.  
  1082.   Note: A None value will be converted to an empty string.
  1083.  
  1084.   Returns:
  1085.     A string in the form 1x|member1|member2|member3...
  1086.   """
  1087.   return '|'.join([urllib.quote_plus(a or '') for a in args])
  1088.  
  1089.  
  1090. def _split_token_parts(blob):
  1091.   """Extracts and unescapes fields from the provided binary string.
  1092.  
  1093.   Reverses the packing performed by _join_token_parts. Used to extract
  1094.   the members of a token object.
  1095.  
  1096.   Note: An empty string from the blob will be interpreted as None.
  1097.  
  1098.   Args:
  1099.     blob: str A string of the form 1x|member1|member2|member3 as created
  1100.         by _join_token_parts
  1101.  
  1102.   Returns:
  1103.     A list of unescaped strings.
  1104.   """
  1105.   return [urllib.unquote_plus(part) or None for part in blob.split('|')]
  1106.  
  1107.  
  1108. def token_to_blob(token):
  1109.   """Serializes the token data as a string for storage in a datastore.
  1110.  
  1111.   Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
  1112.   OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
  1113.   TwoLeggedOAuthHmacToken.
  1114.  
  1115.   Args:
  1116.     token: A token object which must be of one of the supported token classes.
  1117.  
  1118.   Raises:
  1119.     UnsupportedTokenType if the token is not one of the supported token
  1120.     classes listed above.
  1121.  
  1122.   Returns:
  1123.     A string represenging this token. The string can be converted back into
  1124.     an equivalent token object using token_from_blob. Note that any members
  1125.     which are set to '' will be set to None when the token is deserialized
  1126.     by token_from_blob.
  1127.   """
  1128.   if isinstance(token, ClientLoginToken):
  1129.     return _join_token_parts('1c', token.token_string)
  1130.   # Check for secure auth sub type first since it is a subclass of
  1131.   # AuthSubToken.
  1132.   elif isinstance(token, SecureAuthSubToken):
  1133.     return _join_token_parts('1s', token.token_string, token.rsa_private_key,
  1134.                              *token.scopes)
  1135.   elif isinstance(token, AuthSubToken):
  1136.     return _join_token_parts('1a', token.token_string, *token.scopes)
  1137.   elif isinstance(token, TwoLeggedOAuthRsaToken):
  1138.     return _join_token_parts(
  1139.         '1rtl', token.consumer_key, token.rsa_private_key, token.requestor_id)
  1140.   elif isinstance(token, TwoLeggedOAuthHmacToken):
  1141.     return _join_token_parts(
  1142.         '1htl', token.consumer_key, token.consumer_secret, token.requestor_id)
  1143.   # Check RSA OAuth token first since the OAuthRsaToken is a subclass of
  1144.   # OAuthHmacToken.
  1145.   elif isinstance(token, OAuthRsaToken):
  1146.     return _join_token_parts(
  1147.         '1r', token.consumer_key, token.rsa_private_key, token.token,
  1148.         token.token_secret, str(token.auth_state), token.next,
  1149.         token.verifier)
  1150.   elif isinstance(token, OAuthHmacToken):
  1151.     return _join_token_parts(
  1152.         '1h', token.consumer_key, token.consumer_secret, token.token,
  1153.         token.token_secret, str(token.auth_state), token.next,
  1154.         token.verifier)
  1155.   else:
  1156.     raise UnsupportedTokenType(
  1157.         'Unable to serialize token of type %s' % type(token))
  1158.  
  1159.  
  1160. TokenToBlob = token_to_blob
  1161.  
  1162.  
  1163. def token_from_blob(blob):
  1164.   """Deserializes a token string from the datastore back into a token object.
  1165.  
  1166.   Supported token classes: ClientLoginToken, AuthSubToken, SecureAuthSubToken,
  1167.   OAuthRsaToken, and OAuthHmacToken, TwoLeggedOAuthRsaToken,
  1168.   TwoLeggedOAuthHmacToken.
  1169.  
  1170.   Args:
  1171.     blob: string created by token_to_blob.
  1172.  
  1173.   Raises:
  1174.     UnsupportedTokenType if the token is not one of the supported token
  1175.     classes listed above.
  1176.  
  1177.   Returns:
  1178.     A new token object with members set to the values serialized in the
  1179.     blob string. Note that any members which were set to '' in the original
  1180.     token will now be None.
  1181.   """
  1182.   parts = _split_token_parts(blob)
  1183.   if parts[0] == '1c':
  1184.     return ClientLoginToken(parts[1])
  1185.   elif parts[0] == '1a':
  1186.     return AuthSubToken(parts[1], parts[2:])
  1187.   elif parts[0] == '1s':
  1188.     return SecureAuthSubToken(parts[1], parts[2], parts[3:])
  1189.   elif parts[0] == '1rtl':
  1190.     return TwoLeggedOAuthRsaToken(parts[1], parts[2], parts[3])
  1191.   elif parts[0] == '1htl':
  1192.     return TwoLeggedOAuthHmacToken(parts[1], parts[2], parts[3])
  1193.   elif parts[0] == '1r':
  1194.     auth_state = int(parts[5])
  1195.     return OAuthRsaToken(parts[1], parts[2], parts[3], parts[4], auth_state,
  1196.                          parts[6], parts[7])
  1197.   elif parts[0] == '1h':
  1198.     auth_state = int(parts[5])
  1199.     return OAuthHmacToken(parts[1], parts[2], parts[3], parts[4], auth_state,
  1200.                           parts[6], parts[7])
  1201.   else:
  1202.     raise UnsupportedTokenType(
  1203.         'Unable to deserialize token with type marker of %s' % parts[0])
  1204.  
  1205.  
  1206. TokenFromBlob = token_from_blob
  1207.  
  1208.  
  1209. def dump_tokens(tokens):
  1210.   return ','.join([token_to_blob(t) for t in tokens])
  1211.  
  1212.  
  1213. def load_tokens(blob):
  1214.   return [token_from_blob(s) for s in blob.split(',')]
  1215.  
  1216.  
  1217. def find_scopes_for_services(service_names=None):
  1218.   """Creates a combined list of scope URLs for the desired services.
  1219.  
  1220.   This method searches the AUTH_SCOPES dictionary.
  1221.   
  1222.   Args:
  1223.     service_names: list of strings (optional) Each name must be a key in the
  1224.                    AUTH_SCOPES dictionary. If no list is provided (None) then
  1225.                    the resulting list will contain all scope URLs in the
  1226.                    AUTH_SCOPES dict.
  1227.  
  1228.   Returns:
  1229.     A list of URL strings which are the scopes needed to access these services
  1230.     when requesting a token using AuthSub or OAuth.
  1231.   """
  1232.   result_scopes = []
  1233.   if service_names is None:
  1234.     for service_name, scopes in AUTH_SCOPES.iteritems():
  1235.       result_scopes.extend(scopes)
  1236.   else:
  1237.     for service_name in service_names:
  1238.       result_scopes.extend(AUTH_SCOPES[service_name])
  1239.   return result_scopes
  1240.  
  1241.  
  1242. FindScopesForServices = find_scopes_for_services
  1243.  
  1244.  
  1245. def ae_save(token, token_key):
  1246.   """Stores an auth token in the App Engine datastore.
  1247.  
  1248.   This is a convenience method for using the library with App Engine.
  1249.   Recommended usage is to associate the auth token with the current_user.
  1250.   If a user is signed in to the app using the App Engine users API, you
  1251.   can use
  1252.   gdata.gauth.ae_save(some_token, users.get_current_user().user_id())
  1253.   If you are not using the Users API you are free to choose whatever
  1254.   string you would like for a token_string.
  1255.  
  1256.   Args:
  1257.     token: an auth token object. Must be one of ClientLoginToken,
  1258.            AuthSubToken, SecureAuthSubToken, OAuthRsaToken, or OAuthHmacToken
  1259.            (see token_to_blob).
  1260.     token_key: str A unique identified to be used when you want to retrieve
  1261.                the token. If the user is signed in to App Engine using the
  1262.                users API, I recommend using the user ID for the token_key:
  1263.                users.get_current_user().user_id()
  1264.   """
  1265.   import gdata.alt.app_engine
  1266.   key_name = ''.join(('gd_auth_token', token_key))
  1267.   return gdata.alt.app_engine.set_token(key_name, token_to_blob(token))
  1268.  
  1269.  
  1270. AeSave = ae_save
  1271.  
  1272.  
  1273. def ae_load(token_key):
  1274.   """Retrieves a token object from the App Engine datastore.
  1275.  
  1276.   This is a convenience method for using the library with App Engine.
  1277.   See also ae_save.
  1278.  
  1279.   Args:
  1280.     token_key: str The unique key associated with the desired token when it
  1281.                was saved using ae_save.
  1282.  
  1283.   Returns:
  1284.     A token object if there was a token associated with the token_key or None
  1285.     if the key could not be found.
  1286.   """
  1287.   import gdata.alt.app_engine
  1288.   key_name = ''.join(('gd_auth_token', token_key))
  1289.   token_string = gdata.alt.app_engine.get_token(key_name)
  1290.   if token_string is not None:
  1291.     return token_from_blob(token_string)
  1292.   else:
  1293.     return None
  1294.  
  1295.  
  1296. AeLoad = ae_load
  1297.  
  1298.  
  1299. def ae_delete(token_key):
  1300.   """Removes the token object from the App Engine datastore."""
  1301.   import gdata.alt.app_engine
  1302.   key_name = ''.join(('gd_auth_token', token_key))
  1303.   gdata.alt.app_engine.delete_token(key_name)
  1304.  
  1305.  
  1306. AeDelete = ae_delete
  1307.