diff --git a/.gitignore b/src/.gitignore similarity index 100% rename from .gitignore rename to src/.gitignore diff --git a/LICENSE b/src/LICENSE similarity index 100% rename from LICENSE rename to src/LICENSE diff --git a/admin-settings-v1.json b/src/admin-settings-v1.json similarity index 100% rename from admin-settings-v1.json rename to src/admin-settings-v1.json diff --git a/atom/__init__.py b/src/atom/__init__.py similarity index 100% rename from atom/__init__.py rename to src/atom/__init__.py diff --git a/atom/auth.py b/src/atom/auth.py similarity index 100% rename from atom/auth.py rename to src/atom/auth.py diff --git a/atom/client.py b/src/atom/client.py similarity index 100% rename from atom/client.py rename to src/atom/client.py diff --git a/atom/core.py b/src/atom/core.py similarity index 100% rename from atom/core.py rename to src/atom/core.py diff --git a/atom/data.py b/src/atom/data.py similarity index 100% rename from atom/data.py rename to src/atom/data.py diff --git a/atom/http.py b/src/atom/http.py similarity index 100% rename from atom/http.py rename to src/atom/http.py diff --git a/atom/http_core.py b/src/atom/http_core.py similarity index 100% rename from atom/http_core.py rename to src/atom/http_core.py diff --git a/atom/http_interface.py b/src/atom/http_interface.py similarity index 100% rename from atom/http_interface.py rename to src/atom/http_interface.py diff --git a/atom/mock_http.py b/src/atom/mock_http.py similarity index 100% rename from atom/mock_http.py rename to src/atom/mock_http.py diff --git a/atom/mock_http_core.py b/src/atom/mock_http_core.py similarity index 100% rename from atom/mock_http_core.py rename to src/atom/mock_http_core.py diff --git a/atom/mock_service.py b/src/atom/mock_service.py similarity index 100% rename from atom/mock_service.py rename to src/atom/mock_service.py diff --git a/atom/service.py b/src/atom/service.py similarity index 100% rename from atom/service.py rename to src/atom/service.py diff --git a/atom/token_store.py b/src/atom/token_store.py similarity index 100% rename from atom/token_store.py rename to src/atom/token_store.py diff --git a/atom/url.py b/src/atom/url.py similarity index 100% rename from atom/url.py rename to src/atom/url.py diff --git a/build.bat b/src/build.bat similarity index 96% rename from build.bat rename to src/build.bat index 84cd61a9..6a4b4c83 100644 --- a/build.bat +++ b/src/build.bat @@ -1,21 +1,21 @@ -rmdir /q /s gam -rmdir /q /s gam-64 -rmdir /q /s build -rmdir /q /s dist -del /q /f gam-%1-windows.zip -del /q /f gam-%1-windows-x64.zip - -c:\python27-32\scripts\pyinstaller -F --distpath=gam gam.spec -xcopy LICENSE gam\ -xcopy whatsnew.txt gam\ -xcopy admin-settings-v1.json gam\ -xcopy cloudprint-v2.json gam\ -del gam\w9xpopen.exe -"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows.zip gam\ -xr!.svn - -c:\python27\scripts\pyinstaller -F --distpath=gam-64 gam.spec -xcopy LICENSE gam-64\ -xcopy whatsnew.txt gam-64\ -xcopy admin-settings-v1.json gam-64\ -xcopy cloudprint-v2.json gam-64\ -"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn +rmdir /q /s gam +rmdir /q /s gam-64 +rmdir /q /s build +rmdir /q /s dist +del /q /f gam-%1-windows.zip +del /q /f gam-%1-windows-x64.zip + +c:\python27-32\scripts\pyinstaller -F --distpath=gam gam.spec +xcopy LICENSE gam\ +xcopy whatsnew.txt gam\ +xcopy admin-settings-v1.json gam\ +xcopy cloudprint-v2.json gam\ +del gam\w9xpopen.exe +"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows.zip gam\ -xr!.svn + +c:\python27\scripts\pyinstaller -F --distpath=gam-64 gam.spec +xcopy LICENSE gam-64\ +xcopy whatsnew.txt gam-64\ +xcopy admin-settings-v1.json gam-64\ +xcopy cloudprint-v2.json gam-64\ +"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn diff --git a/cloudprint-v2.json b/src/cloudprint-v2.json similarity index 100% rename from cloudprint-v2.json rename to src/cloudprint-v2.json diff --git a/gam.py b/src/gam.py old mode 100755 new mode 100644 similarity index 100% rename from gam.py rename to src/gam.py diff --git a/gam.spec b/src/gam.spec similarity index 100% rename from gam.spec rename to src/gam.spec diff --git a/gdata/__init__.py b/src/gdata/__init__.py similarity index 100% rename from gdata/__init__.py rename to src/gdata/__init__.py diff --git a/gdata/alt/__init__.py b/src/gdata/alt/__init__.py similarity index 100% rename from gdata/alt/__init__.py rename to src/gdata/alt/__init__.py diff --git a/gdata/alt/app_engine.py b/src/gdata/alt/app_engine.py similarity index 100% rename from gdata/alt/app_engine.py rename to src/gdata/alt/app_engine.py diff --git a/gdata/alt/appengine.py b/src/gdata/alt/appengine.py similarity index 100% rename from gdata/alt/appengine.py rename to src/gdata/alt/appengine.py diff --git a/gdata/apps/__init__.py b/src/gdata/apps/__init__.py similarity index 100% rename from gdata/apps/__init__.py rename to src/gdata/apps/__init__.py diff --git a/gdata/apps/adminsettings/__init__.py b/src/gdata/apps/adminsettings/__init__.py similarity index 100% rename from gdata/apps/adminsettings/__init__.py rename to src/gdata/apps/adminsettings/__init__.py diff --git a/gdata/apps/adminsettings/service.py b/src/gdata/apps/adminsettings/service.py similarity index 100% rename from gdata/apps/adminsettings/service.py rename to src/gdata/apps/adminsettings/service.py diff --git a/gdata/apps/audit/__init__.py b/src/gdata/apps/audit/__init__.py similarity index 100% rename from gdata/apps/audit/__init__.py rename to src/gdata/apps/audit/__init__.py diff --git a/gdata/apps/audit/service.py b/src/gdata/apps/audit/service.py similarity index 100% rename from gdata/apps/audit/service.py rename to src/gdata/apps/audit/service.py diff --git a/gdata/apps/emailsettings/__init__.py b/src/gdata/apps/emailsettings/__init__.py similarity index 100% rename from gdata/apps/emailsettings/__init__.py rename to src/gdata/apps/emailsettings/__init__.py diff --git a/gdata/apps/emailsettings/service.py b/src/gdata/apps/emailsettings/service.py similarity index 100% rename from gdata/apps/emailsettings/service.py rename to src/gdata/apps/emailsettings/service.py diff --git a/gdata/apps/res_cal/__init__.py b/src/gdata/apps/res_cal/__init__.py similarity index 100% rename from gdata/apps/res_cal/__init__.py rename to src/gdata/apps/res_cal/__init__.py diff --git a/gdata/apps/res_cal/service.py b/src/gdata/apps/res_cal/service.py similarity index 100% rename from gdata/apps/res_cal/service.py rename to src/gdata/apps/res_cal/service.py diff --git a/gdata/apps/service.py b/src/gdata/apps/service.py similarity index 100% rename from gdata/apps/service.py rename to src/gdata/apps/service.py diff --git a/gdata/auth.py b/src/gdata/auth.py similarity index 100% rename from gdata/auth.py rename to src/gdata/auth.py diff --git a/gdata/gauth.py b/src/gdata/gauth.py similarity index 100% rename from gdata/gauth.py rename to src/gdata/gauth.py diff --git a/gdata/oauth/CHANGES.txt b/src/gdata/oauth/CHANGES.txt similarity index 93% rename from gdata/oauth/CHANGES.txt rename to src/gdata/oauth/CHANGES.txt index 7c2b92cd..90c4bb8e 100644 --- a/gdata/oauth/CHANGES.txt +++ b/src/gdata/oauth/CHANGES.txt @@ -1,17 +1,17 @@ -1. Moved oauth.py to __init__.py - -2. Refactored __init__.py for compatibility with python 2.2 (Issue 59) - -3. Refactored rsa.py for compatibility with python 2.2 (Issue 59) - -4. Refactored OAuthRequest.from_token_and_callback since the callback url was -getting double url-encoding the callback url in place of single. (Issue 43) - -5. Added build_signature_base_string method to rsa.py since it used the -implementation of this method from oauth.OAuthSignatureMethod_HMAC_SHA1 which -was incorrect since it enforced the presence of a consumer secret and a token -secret. Also, changed its super class from oauth.OAuthSignatureMethod_HMAC_SHA1 -to oauth.OAuthSignatureMethod (Issue 64) - -6. Refactored .to_header method since it returned non-oauth params +1. Moved oauth.py to __init__.py + +2. Refactored __init__.py for compatibility with python 2.2 (Issue 59) + +3. Refactored rsa.py for compatibility with python 2.2 (Issue 59) + +4. Refactored OAuthRequest.from_token_and_callback since the callback url was +getting double url-encoding the callback url in place of single. (Issue 43) + +5. Added build_signature_base_string method to rsa.py since it used the +implementation of this method from oauth.OAuthSignatureMethod_HMAC_SHA1 which +was incorrect since it enforced the presence of a consumer secret and a token +secret. Also, changed its super class from oauth.OAuthSignatureMethod_HMAC_SHA1 +to oauth.OAuthSignatureMethod (Issue 64) + +6. Refactored .to_header method since it returned non-oauth params as well which was incorrect. (Issue 31) \ No newline at end of file diff --git a/gdata/oauth/__init__.py b/src/gdata/oauth/__init__.py similarity index 97% rename from gdata/oauth/__init__.py rename to src/gdata/oauth/__init__.py index baf543ed..fd1f6a52 100644 --- a/gdata/oauth/__init__.py +++ b/src/gdata/oauth/__init__.py @@ -1,524 +1,524 @@ -import cgi -import urllib -import time -import random -import urlparse -import hmac -import binascii - -VERSION = '1.0' # Hi Blaine! -HTTP_METHOD = 'GET' -SIGNATURE_METHOD = 'PLAINTEXT' - -# Generic exception class -class OAuthError(RuntimeError): - def __init__(self, message='OAuth error occured.'): - self.message = message - -# optional WWW-Authenticate header (401 error) -def build_authenticate_header(realm=''): - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - -# url escape -def escape(s): - # escape '/' too - return urllib.quote(s, safe='~') - -# util function: current timestamp -# seconds since epoch (UTC) -def generate_timestamp(): - return int(time.time()) - -# util function: nonce -# pseudorandom number -def generate_nonce(length=8): - return ''.join([str(random.randint(0, 9)) for i in range(length)]) - -# OAuthConsumer is a data type that represents the identity of the Consumer -# via its shared secret with the Service Provider. -class OAuthConsumer(object): - key = None - secret = None - - def __init__(self, key, secret): - self.key = key - self.secret = secret - -# OAuthToken is a data type that represents an End User via either an access -# or request token. -class OAuthToken(object): - # access tokens and request tokens - key = None - secret = None - - ''' - key = the token - secret = the token secret - ''' - def __init__(self, key, secret): - self.key = key - self.secret = secret - - def to_string(self): - return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) - - # return a token from something like: - # oauth_token_secret=digg&oauth_token=digg - def from_string(s): - params = cgi.parse_qs(s, keep_blank_values=False) - key = params['oauth_token'][0] - secret = params['oauth_token_secret'][0] - return OAuthToken(key, secret) - from_string = staticmethod(from_string) - - def __str__(self): - return self.to_string() - -# OAuthRequest represents the request and can be serialized -class OAuthRequest(object): - ''' - OAuth parameters: - - oauth_consumer_key - - oauth_token - - oauth_signature_method - - oauth_signature - - oauth_timestamp - - oauth_nonce - - oauth_version - ... any additional parameters, as defined by the Service Provider. - ''' - parameters = None # oauth parameters - http_method = HTTP_METHOD - http_url = None - version = VERSION - - def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): - self.http_method = http_method - self.http_url = http_url - self.parameters = parameters or {} - - def set_parameter(self, parameter, value): - self.parameters[parameter] = value - - def get_parameter(self, parameter): - try: - return self.parameters[parameter] - except: - raise OAuthError('Parameter not found: %s' % parameter) - - def _get_timestamp_nonce(self): - return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') - - # get any non-oauth parameters - def get_nonoauth_parameters(self): - parameters = {} - for k, v in self.parameters.iteritems(): - # ignore oauth parameters - if k.find('oauth_') < 0: - parameters[k] = v - return parameters - - # serialize as a header for an HTTPAuth request - def to_header(self, realm=''): - auth_header = 'OAuth realm="%s"' % realm - # add the oauth parameters - if self.parameters: - for k, v in self.parameters.iteritems(): - if k[:6] == 'oauth_': - auth_header += ', %s="%s"' % (k, escape(str(v))) - return {'Authorization': auth_header} - - # serialize as post data for a POST request - def to_postdata(self): - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) - - # serialize as a url for a GET request - def to_url(self): - return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) - - # return a string that consists of all the parameters that need to be signed - def get_normalized_parameters(self): - params = self.parameters - try: - # exclude the signature if it exists - del params['oauth_signature'] - except: - pass - key_values = params.items() - # sort lexicographically, first after key, then after value - key_values.sort() - # combine key value pairs in string and escape - return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) - - # just uppercases the http method - def get_normalized_http_method(self): - return self.http_method.upper() - - # parses the url and rebuilds it to be scheme://host/path - def get_normalized_http_url(self): - parts = urlparse.urlparse(self.http_url) - url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path - return url_string - - # set the signature parameter to the result of build_signature - def sign_request(self, signature_method, consumer, token): - # set the signature method - self.set_parameter('oauth_signature_method', signature_method.get_name()) - # set the signature - self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) - - def build_signature(self, signature_method, consumer, token): - # call the build signature method within the signature method - return signature_method.build_signature(self, consumer, token) - - def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): - # combine multiple parameter sources - if parameters is None: - parameters = {} - - # headers - if headers and 'Authorization' in headers: - auth_header = headers['Authorization'] - # check that the authorization header is OAuth - if auth_header.index('OAuth') > -1: - try: - # get the parameters from the header - header_params = OAuthRequest._split_header(auth_header) - parameters.update(header_params) - except: - raise OAuthError('Unable to parse OAuth parameters from Authorization header.') - - # GET or POST query string - if query_string: - query_params = OAuthRequest._split_url_string(query_string) - parameters.update(query_params) - - # URL parameters - param_str = urlparse.urlparse(http_url)[4] # query - url_params = OAuthRequest._split_url_string(param_str) - parameters.update(url_params) - - if parameters: - return OAuthRequest(http_method, http_url, parameters) - - return None - from_request = staticmethod(from_request) - - def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): - if not parameters: - parameters = {} - - defaults = { - 'oauth_consumer_key': oauth_consumer.key, - 'oauth_timestamp': generate_timestamp(), - 'oauth_nonce': generate_nonce(), - 'oauth_version': OAuthRequest.version, - } - - defaults.update(parameters) - parameters = defaults - - if token: - parameters['oauth_token'] = token.key - - return OAuthRequest(http_method, http_url, parameters) - from_consumer_and_token = staticmethod(from_consumer_and_token) - - def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): - if not parameters: - parameters = {} - - parameters['oauth_token'] = token.key - - if callback: - parameters['oauth_callback'] = callback - - return OAuthRequest(http_method, http_url, parameters) - from_token_and_callback = staticmethod(from_token_and_callback) - - # util function: turn Authorization: header into parameters, has to do some unescaping - def _split_header(header): - params = {} - parts = header.split(',') - for param in parts: - # ignore realm parameter - if param.find('OAuth realm') > -1: - continue - # remove whitespace - param = param.strip() - # split key-value - param_parts = param.split('=', 1) - # remove quotes and unescape the value - params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) - return params - _split_header = staticmethod(_split_header) - - # util function: turn url string into parameters, has to do some unescaping - def _split_url_string(param_str): - parameters = cgi.parse_qs(param_str, keep_blank_values=False) - for k, v in parameters.iteritems(): - parameters[k] = urllib.unquote(v[0]) - return parameters - _split_url_string = staticmethod(_split_url_string) - -# OAuthServer is a worker to check a requests validity against a data store -class OAuthServer(object): - timestamp_threshold = 300 # in seconds, five minutes - version = VERSION - signature_methods = None - data_store = None - - def __init__(self, data_store=None, signature_methods=None): - self.data_store = data_store - self.signature_methods = signature_methods or {} - - def set_data_store(self, oauth_data_store): - self.data_store = data_store - - def get_data_store(self): - return self.data_store - - def add_signature_method(self, signature_method): - self.signature_methods[signature_method.get_name()] = signature_method - return self.signature_methods - - # process a request_token request - # returns the request token on success - def fetch_request_token(self, oauth_request): - try: - # get the request token for authorization - token = self._get_token(oauth_request, 'request') - except OAuthError: - # no token required for the initial token request - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - self._check_signature(oauth_request, consumer, None) - # fetch a new token - token = self.data_store.fetch_request_token(consumer) - return token - - # process an access_token request - # returns the access token on success - def fetch_access_token(self, oauth_request): - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - # get the request token - token = self._get_token(oauth_request, 'request') - self._check_signature(oauth_request, consumer, token) - new_token = self.data_store.fetch_access_token(consumer, token) - return new_token - - # verify an api call, checks all the parameters - def verify_request(self, oauth_request): - # -> consumer and token - version = self._get_version(oauth_request) - consumer = self._get_consumer(oauth_request) - # get the access token - token = self._get_token(oauth_request, 'access') - self._check_signature(oauth_request, consumer, token) - parameters = oauth_request.get_nonoauth_parameters() - return consumer, token, parameters - - # authorize a request token - def authorize_token(self, token, user): - return self.data_store.authorize_request_token(token, user) - - # get the callback url - def get_callback(self, oauth_request): - return oauth_request.get_parameter('oauth_callback') - - # optional support for the authenticate header - def build_authenticate_header(self, realm=''): - return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} - - # verify the correct version request for this server - def _get_version(self, oauth_request): - try: - version = oauth_request.get_parameter('oauth_version') - except: - version = VERSION - if version and version != self.version: - raise OAuthError('OAuth version %s not supported.' % str(version)) - return version - - # figure out the signature with some defaults - def _get_signature_method(self, oauth_request): - try: - signature_method = oauth_request.get_parameter('oauth_signature_method') - except: - signature_method = SIGNATURE_METHOD - try: - # get the signature method object - signature_method = self.signature_methods[signature_method] - except: - signature_method_names = ', '.join(self.signature_methods.keys()) - raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) - - return signature_method - - def _get_consumer(self, oauth_request): - consumer_key = oauth_request.get_parameter('oauth_consumer_key') - if not consumer_key: - raise OAuthError('Invalid consumer key.') - consumer = self.data_store.lookup_consumer(consumer_key) - if not consumer: - raise OAuthError('Invalid consumer.') - return consumer - - # try to find the token for the provided request token key - def _get_token(self, oauth_request, token_type='access'): - token_field = oauth_request.get_parameter('oauth_token') - token = self.data_store.lookup_token(token_type, token_field) - if not token: - raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) - return token - - def _check_signature(self, oauth_request, consumer, token): - timestamp, nonce = oauth_request._get_timestamp_nonce() - self._check_timestamp(timestamp) - self._check_nonce(consumer, token, nonce) - signature_method = self._get_signature_method(oauth_request) - try: - signature = oauth_request.get_parameter('oauth_signature') - except: - raise OAuthError('Missing signature.') - # validate the signature - valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) - if not valid_sig: - key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) - raise OAuthError('Invalid signature. Expected signature base string: %s' % base) - built = signature_method.build_signature(oauth_request, consumer, token) - - def _check_timestamp(self, timestamp): - # verify that timestamp is recentish - timestamp = int(timestamp) - now = int(time.time()) - lapsed = now - timestamp - if lapsed > self.timestamp_threshold: - raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) - - def _check_nonce(self, consumer, token, nonce): - # verify that the nonce is uniqueish - nonce = self.data_store.lookup_nonce(consumer, token, nonce) - if nonce: - raise OAuthError('Nonce already used: %s' % str(nonce)) - -# OAuthClient is a worker to attempt to execute a request -class OAuthClient(object): - consumer = None - token = None - - def __init__(self, oauth_consumer, oauth_token): - self.consumer = oauth_consumer - self.token = oauth_token - - def get_consumer(self): - return self.consumer - - def get_token(self): - return self.token - - def fetch_request_token(self, oauth_request): - # -> OAuthToken - raise NotImplementedError - - def fetch_access_token(self, oauth_request): - # -> OAuthToken - raise NotImplementedError - - def access_resource(self, oauth_request): - # -> some protected resource - raise NotImplementedError - -# OAuthDataStore is a database abstraction used to lookup consumers and tokens -class OAuthDataStore(object): - - def lookup_consumer(self, key): - # -> OAuthConsumer - raise NotImplementedError - - def lookup_token(self, oauth_consumer, token_type, token_token): - # -> OAuthToken - raise NotImplementedError - - def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): - # -> OAuthToken - raise NotImplementedError - - def fetch_request_token(self, oauth_consumer): - # -> OAuthToken - raise NotImplementedError - - def fetch_access_token(self, oauth_consumer, oauth_token): - # -> OAuthToken - raise NotImplementedError - - def authorize_request_token(self, oauth_token, user): - # -> OAuthToken - raise NotImplementedError - -# OAuthSignatureMethod is a strategy class that implements a signature method -class OAuthSignatureMethod(object): - def get_name(self): - # -> str - raise NotImplementedError - - def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): - # -> str key, str raw - raise NotImplementedError - - def build_signature(self, oauth_request, oauth_consumer, oauth_token): - # -> str - raise NotImplementedError - - def check_signature(self, oauth_request, consumer, token, signature): - built = self.build_signature(oauth_request, consumer, token) - return built == signature - -class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): - - def get_name(self): - return 'HMAC-SHA1' - - def build_signature_base_string(self, oauth_request, consumer, token): - sig = ( - escape(oauth_request.get_normalized_http_method()), - escape(oauth_request.get_normalized_http_url()), - escape(oauth_request.get_normalized_parameters()), - ) - - key = '%s&' % escape(consumer.secret) - if token: - key += escape(token.secret) - raw = '&'.join(sig) - return key, raw - - def build_signature(self, oauth_request, consumer, token): - # build the base signature string - key, raw = self.build_signature_base_string(oauth_request, consumer, token) - - # hmac object - try: - import hashlib # 2.5 - hashed = hmac.new(key, raw, hashlib.sha1) - except: - import sha # deprecated - hashed = hmac.new(key, raw, sha) - - # calculate the digest base 64 - return binascii.b2a_base64(hashed.digest())[:-1] - -class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): - - def get_name(self): - return 'PLAINTEXT' - - def build_signature_base_string(self, oauth_request, consumer, token): - # concatenate the consumer key and secret - sig = escape(consumer.secret) + '&' - if token: - sig = sig + escape(token.secret) - return sig - - def build_signature(self, oauth_request, consumer, token): - return self.build_signature_base_string(oauth_request, consumer, token) +import cgi +import urllib +import time +import random +import urlparse +import hmac +import binascii + +VERSION = '1.0' # Hi Blaine! +HTTP_METHOD = 'GET' +SIGNATURE_METHOD = 'PLAINTEXT' + +# Generic exception class +class OAuthError(RuntimeError): + def __init__(self, message='OAuth error occured.'): + self.message = message + +# optional WWW-Authenticate header (401 error) +def build_authenticate_header(realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + +# url escape +def escape(s): + # escape '/' too + return urllib.quote(s, safe='~') + +# util function: current timestamp +# seconds since epoch (UTC) +def generate_timestamp(): + return int(time.time()) + +# util function: nonce +# pseudorandom number +def generate_nonce(length=8): + return ''.join([str(random.randint(0, 9)) for i in range(length)]) + +# OAuthConsumer is a data type that represents the identity of the Consumer +# via its shared secret with the Service Provider. +class OAuthConsumer(object): + key = None + secret = None + + def __init__(self, key, secret): + self.key = key + self.secret = secret + +# OAuthToken is a data type that represents an End User via either an access +# or request token. +class OAuthToken(object): + # access tokens and request tokens + key = None + secret = None + + ''' + key = the token + secret = the token secret + ''' + def __init__(self, key, secret): + self.key = key + self.secret = secret + + def to_string(self): + return urllib.urlencode({'oauth_token': self.key, 'oauth_token_secret': self.secret}) + + # return a token from something like: + # oauth_token_secret=digg&oauth_token=digg + def from_string(s): + params = cgi.parse_qs(s, keep_blank_values=False) + key = params['oauth_token'][0] + secret = params['oauth_token_secret'][0] + return OAuthToken(key, secret) + from_string = staticmethod(from_string) + + def __str__(self): + return self.to_string() + +# OAuthRequest represents the request and can be serialized +class OAuthRequest(object): + ''' + OAuth parameters: + - oauth_consumer_key + - oauth_token + - oauth_signature_method + - oauth_signature + - oauth_timestamp + - oauth_nonce + - oauth_version + ... any additional parameters, as defined by the Service Provider. + ''' + parameters = None # oauth parameters + http_method = HTTP_METHOD + http_url = None + version = VERSION + + def __init__(self, http_method=HTTP_METHOD, http_url=None, parameters=None): + self.http_method = http_method + self.http_url = http_url + self.parameters = parameters or {} + + def set_parameter(self, parameter, value): + self.parameters[parameter] = value + + def get_parameter(self, parameter): + try: + return self.parameters[parameter] + except: + raise OAuthError('Parameter not found: %s' % parameter) + + def _get_timestamp_nonce(self): + return self.get_parameter('oauth_timestamp'), self.get_parameter('oauth_nonce') + + # get any non-oauth parameters + def get_nonoauth_parameters(self): + parameters = {} + for k, v in self.parameters.iteritems(): + # ignore oauth parameters + if k.find('oauth_') < 0: + parameters[k] = v + return parameters + + # serialize as a header for an HTTPAuth request + def to_header(self, realm=''): + auth_header = 'OAuth realm="%s"' % realm + # add the oauth parameters + if self.parameters: + for k, v in self.parameters.iteritems(): + if k[:6] == 'oauth_': + auth_header += ', %s="%s"' % (k, escape(str(v))) + return {'Authorization': auth_header} + + # serialize as post data for a POST request + def to_postdata(self): + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in self.parameters.iteritems()]) + + # serialize as a url for a GET request + def to_url(self): + return '%s?%s' % (self.get_normalized_http_url(), self.to_postdata()) + + # return a string that consists of all the parameters that need to be signed + def get_normalized_parameters(self): + params = self.parameters + try: + # exclude the signature if it exists + del params['oauth_signature'] + except: + pass + key_values = params.items() + # sort lexicographically, first after key, then after value + key_values.sort() + # combine key value pairs in string and escape + return '&'.join(['%s=%s' % (escape(str(k)), escape(str(v))) for k, v in key_values]) + + # just uppercases the http method + def get_normalized_http_method(self): + return self.http_method.upper() + + # parses the url and rebuilds it to be scheme://host/path + def get_normalized_http_url(self): + parts = urlparse.urlparse(self.http_url) + url_string = '%s://%s%s' % (parts[0], parts[1], parts[2]) # scheme, netloc, path + return url_string + + # set the signature parameter to the result of build_signature + def sign_request(self, signature_method, consumer, token): + # set the signature method + self.set_parameter('oauth_signature_method', signature_method.get_name()) + # set the signature + self.set_parameter('oauth_signature', self.build_signature(signature_method, consumer, token)) + + def build_signature(self, signature_method, consumer, token): + # call the build signature method within the signature method + return signature_method.build_signature(self, consumer, token) + + def from_request(http_method, http_url, headers=None, parameters=None, query_string=None): + # combine multiple parameter sources + if parameters is None: + parameters = {} + + # headers + if headers and 'Authorization' in headers: + auth_header = headers['Authorization'] + # check that the authorization header is OAuth + if auth_header.index('OAuth') > -1: + try: + # get the parameters from the header + header_params = OAuthRequest._split_header(auth_header) + parameters.update(header_params) + except: + raise OAuthError('Unable to parse OAuth parameters from Authorization header.') + + # GET or POST query string + if query_string: + query_params = OAuthRequest._split_url_string(query_string) + parameters.update(query_params) + + # URL parameters + param_str = urlparse.urlparse(http_url)[4] # query + url_params = OAuthRequest._split_url_string(param_str) + parameters.update(url_params) + + if parameters: + return OAuthRequest(http_method, http_url, parameters) + + return None + from_request = staticmethod(from_request) + + def from_consumer_and_token(oauth_consumer, token=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + defaults = { + 'oauth_consumer_key': oauth_consumer.key, + 'oauth_timestamp': generate_timestamp(), + 'oauth_nonce': generate_nonce(), + 'oauth_version': OAuthRequest.version, + } + + defaults.update(parameters) + parameters = defaults + + if token: + parameters['oauth_token'] = token.key + + return OAuthRequest(http_method, http_url, parameters) + from_consumer_and_token = staticmethod(from_consumer_and_token) + + def from_token_and_callback(token, callback=None, http_method=HTTP_METHOD, http_url=None, parameters=None): + if not parameters: + parameters = {} + + parameters['oauth_token'] = token.key + + if callback: + parameters['oauth_callback'] = callback + + return OAuthRequest(http_method, http_url, parameters) + from_token_and_callback = staticmethod(from_token_and_callback) + + # util function: turn Authorization: header into parameters, has to do some unescaping + def _split_header(header): + params = {} + parts = header.split(',') + for param in parts: + # ignore realm parameter + if param.find('OAuth realm') > -1: + continue + # remove whitespace + param = param.strip() + # split key-value + param_parts = param.split('=', 1) + # remove quotes and unescape the value + params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) + return params + _split_header = staticmethod(_split_header) + + # util function: turn url string into parameters, has to do some unescaping + def _split_url_string(param_str): + parameters = cgi.parse_qs(param_str, keep_blank_values=False) + for k, v in parameters.iteritems(): + parameters[k] = urllib.unquote(v[0]) + return parameters + _split_url_string = staticmethod(_split_url_string) + +# OAuthServer is a worker to check a requests validity against a data store +class OAuthServer(object): + timestamp_threshold = 300 # in seconds, five minutes + version = VERSION + signature_methods = None + data_store = None + + def __init__(self, data_store=None, signature_methods=None): + self.data_store = data_store + self.signature_methods = signature_methods or {} + + def set_data_store(self, oauth_data_store): + self.data_store = data_store + + def get_data_store(self): + return self.data_store + + def add_signature_method(self, signature_method): + self.signature_methods[signature_method.get_name()] = signature_method + return self.signature_methods + + # process a request_token request + # returns the request token on success + def fetch_request_token(self, oauth_request): + try: + # get the request token for authorization + token = self._get_token(oauth_request, 'request') + except OAuthError: + # no token required for the initial token request + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + self._check_signature(oauth_request, consumer, None) + # fetch a new token + token = self.data_store.fetch_request_token(consumer) + return token + + # process an access_token request + # returns the access token on success + def fetch_access_token(self, oauth_request): + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the request token + token = self._get_token(oauth_request, 'request') + self._check_signature(oauth_request, consumer, token) + new_token = self.data_store.fetch_access_token(consumer, token) + return new_token + + # verify an api call, checks all the parameters + def verify_request(self, oauth_request): + # -> consumer and token + version = self._get_version(oauth_request) + consumer = self._get_consumer(oauth_request) + # get the access token + token = self._get_token(oauth_request, 'access') + self._check_signature(oauth_request, consumer, token) + parameters = oauth_request.get_nonoauth_parameters() + return consumer, token, parameters + + # authorize a request token + def authorize_token(self, token, user): + return self.data_store.authorize_request_token(token, user) + + # get the callback url + def get_callback(self, oauth_request): + return oauth_request.get_parameter('oauth_callback') + + # optional support for the authenticate header + def build_authenticate_header(self, realm=''): + return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} + + # verify the correct version request for this server + def _get_version(self, oauth_request): + try: + version = oauth_request.get_parameter('oauth_version') + except: + version = VERSION + if version and version != self.version: + raise OAuthError('OAuth version %s not supported.' % str(version)) + return version + + # figure out the signature with some defaults + def _get_signature_method(self, oauth_request): + try: + signature_method = oauth_request.get_parameter('oauth_signature_method') + except: + signature_method = SIGNATURE_METHOD + try: + # get the signature method object + signature_method = self.signature_methods[signature_method] + except: + signature_method_names = ', '.join(self.signature_methods.keys()) + raise OAuthError('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) + + return signature_method + + def _get_consumer(self, oauth_request): + consumer_key = oauth_request.get_parameter('oauth_consumer_key') + if not consumer_key: + raise OAuthError('Invalid consumer key.') + consumer = self.data_store.lookup_consumer(consumer_key) + if not consumer: + raise OAuthError('Invalid consumer.') + return consumer + + # try to find the token for the provided request token key + def _get_token(self, oauth_request, token_type='access'): + token_field = oauth_request.get_parameter('oauth_token') + token = self.data_store.lookup_token(token_type, token_field) + if not token: + raise OAuthError('Invalid %s token: %s' % (token_type, token_field)) + return token + + def _check_signature(self, oauth_request, consumer, token): + timestamp, nonce = oauth_request._get_timestamp_nonce() + self._check_timestamp(timestamp) + self._check_nonce(consumer, token, nonce) + signature_method = self._get_signature_method(oauth_request) + try: + signature = oauth_request.get_parameter('oauth_signature') + except: + raise OAuthError('Missing signature.') + # validate the signature + valid_sig = signature_method.check_signature(oauth_request, consumer, token, signature) + if not valid_sig: + key, base = signature_method.build_signature_base_string(oauth_request, consumer, token) + raise OAuthError('Invalid signature. Expected signature base string: %s' % base) + built = signature_method.build_signature(oauth_request, consumer, token) + + def _check_timestamp(self, timestamp): + # verify that timestamp is recentish + timestamp = int(timestamp) + now = int(time.time()) + lapsed = now - timestamp + if lapsed > self.timestamp_threshold: + raise OAuthError('Expired timestamp: given %d and now %s has a greater difference than threshold %d' % (timestamp, now, self.timestamp_threshold)) + + def _check_nonce(self, consumer, token, nonce): + # verify that the nonce is uniqueish + nonce = self.data_store.lookup_nonce(consumer, token, nonce) + if nonce: + raise OAuthError('Nonce already used: %s' % str(nonce)) + +# OAuthClient is a worker to attempt to execute a request +class OAuthClient(object): + consumer = None + token = None + + def __init__(self, oauth_consumer, oauth_token): + self.consumer = oauth_consumer + self.token = oauth_token + + def get_consumer(self): + return self.consumer + + def get_token(self): + return self.token + + def fetch_request_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_request): + # -> OAuthToken + raise NotImplementedError + + def access_resource(self, oauth_request): + # -> some protected resource + raise NotImplementedError + +# OAuthDataStore is a database abstraction used to lookup consumers and tokens +class OAuthDataStore(object): + + def lookup_consumer(self, key): + # -> OAuthConsumer + raise NotImplementedError + + def lookup_token(self, oauth_consumer, token_type, token_token): + # -> OAuthToken + raise NotImplementedError + + def lookup_nonce(self, oauth_consumer, oauth_token, nonce, timestamp): + # -> OAuthToken + raise NotImplementedError + + def fetch_request_token(self, oauth_consumer): + # -> OAuthToken + raise NotImplementedError + + def fetch_access_token(self, oauth_consumer, oauth_token): + # -> OAuthToken + raise NotImplementedError + + def authorize_request_token(self, oauth_token, user): + # -> OAuthToken + raise NotImplementedError + +# OAuthSignatureMethod is a strategy class that implements a signature method +class OAuthSignatureMethod(object): + def get_name(self): + # -> str + raise NotImplementedError + + def build_signature_base_string(self, oauth_request, oauth_consumer, oauth_token): + # -> str key, str raw + raise NotImplementedError + + def build_signature(self, oauth_request, oauth_consumer, oauth_token): + # -> str + raise NotImplementedError + + def check_signature(self, oauth_request, consumer, token, signature): + built = self.build_signature(oauth_request, consumer, token) + return built == signature + +class OAuthSignatureMethod_HMAC_SHA1(OAuthSignatureMethod): + + def get_name(self): + return 'HMAC-SHA1' + + def build_signature_base_string(self, oauth_request, consumer, token): + sig = ( + escape(oauth_request.get_normalized_http_method()), + escape(oauth_request.get_normalized_http_url()), + escape(oauth_request.get_normalized_parameters()), + ) + + key = '%s&' % escape(consumer.secret) + if token: + key += escape(token.secret) + raw = '&'.join(sig) + return key, raw + + def build_signature(self, oauth_request, consumer, token): + # build the base signature string + key, raw = self.build_signature_base_string(oauth_request, consumer, token) + + # hmac object + try: + import hashlib # 2.5 + hashed = hmac.new(key, raw, hashlib.sha1) + except: + import sha # deprecated + hashed = hmac.new(key, raw, sha) + + # calculate the digest base 64 + return binascii.b2a_base64(hashed.digest())[:-1] + +class OAuthSignatureMethod_PLAINTEXT(OAuthSignatureMethod): + + def get_name(self): + return 'PLAINTEXT' + + def build_signature_base_string(self, oauth_request, consumer, token): + # concatenate the consumer key and secret + sig = escape(consumer.secret) + '&' + if token: + sig = sig + escape(token.secret) + return sig + + def build_signature(self, oauth_request, consumer, token): + return self.build_signature_base_string(oauth_request, consumer, token) diff --git a/gdata/oauth/rsa.py b/src/gdata/oauth/rsa.py similarity index 100% rename from gdata/oauth/rsa.py rename to src/gdata/oauth/rsa.py diff --git a/gdata/service.py b/src/gdata/service.py similarity index 100% rename from gdata/service.py rename to src/gdata/service.py diff --git a/gdata/tlslite/BaseDB.py b/src/gdata/tlslite/BaseDB.py similarity index 95% rename from gdata/tlslite/BaseDB.py rename to src/gdata/tlslite/BaseDB.py index ca8dff6b..4988c150 100644 --- a/gdata/tlslite/BaseDB.py +++ b/src/gdata/tlslite/BaseDB.py @@ -1,120 +1,120 @@ -"""Base class for SharedKeyDB and VerifierDB.""" - -import anydbm -import thread - -class BaseDB: - def __init__(self, filename, type): - self.type = type - self.filename = filename - if self.filename: - self.db = None - else: - self.db = {} - self.lock = thread.allocate_lock() - - def create(self): - """Create a new on-disk database. - - @raise anydbm.error: If there's a problem creating the database. - """ - if self.filename: - self.db = anydbm.open(self.filename, "n") #raises anydbm.error - self.db["--Reserved--type"] = self.type - self.db.sync() - else: - self.db = {} - - def open(self): - """Open a pre-existing on-disk database. - - @raise anydbm.error: If there's a problem opening the database. - @raise ValueError: If the database is not of the right type. - """ - if not self.filename: - raise ValueError("Can only open on-disk databases") - self.db = anydbm.open(self.filename, "w") #raises anydbm.error - try: - if self.db["--Reserved--type"] != self.type: - raise ValueError("Not a %s database" % self.type) - except KeyError: - raise ValueError("Not a recognized database") - - def __getitem__(self, username): - if self.db == None: - raise AssertionError("DB not open") - - self.lock.acquire() - try: - valueStr = self.db[username] - finally: - self.lock.release() - - return self._getItem(username, valueStr) - - def __setitem__(self, username, value): - if self.db == None: - raise AssertionError("DB not open") - - valueStr = self._setItem(username, value) - - self.lock.acquire() - try: - self.db[username] = valueStr - if self.filename: - self.db.sync() - finally: - self.lock.release() - - def __delitem__(self, username): - if self.db == None: - raise AssertionError("DB not open") - - self.lock.acquire() - try: - del(self.db[username]) - if self.filename: - self.db.sync() - finally: - self.lock.release() - - def __contains__(self, username): - """Check if the database contains the specified username. - - @type username: str - @param username: The username to check for. - - @rtype: bool - @return: True if the database contains the username, False - otherwise. - - """ - if self.db == None: - raise AssertionError("DB not open") - - self.lock.acquire() - try: - return self.db.has_key(username) - finally: - self.lock.release() - - def check(self, username, param): - value = self.__getitem__(username) - return self._checkItem(value, username, param) - - def keys(self): - """Return a list of usernames in the database. - - @rtype: list - @return: The usernames in the database. - """ - if self.db == None: - raise AssertionError("DB not open") - - self.lock.acquire() - try: - usernames = self.db.keys() - finally: - self.lock.release() - usernames = [u for u in usernames if not u.startswith("--Reserved--")] +"""Base class for SharedKeyDB and VerifierDB.""" + +import anydbm +import thread + +class BaseDB: + def __init__(self, filename, type): + self.type = type + self.filename = filename + if self.filename: + self.db = None + else: + self.db = {} + self.lock = thread.allocate_lock() + + def create(self): + """Create a new on-disk database. + + @raise anydbm.error: If there's a problem creating the database. + """ + if self.filename: + self.db = anydbm.open(self.filename, "n") #raises anydbm.error + self.db["--Reserved--type"] = self.type + self.db.sync() + else: + self.db = {} + + def open(self): + """Open a pre-existing on-disk database. + + @raise anydbm.error: If there's a problem opening the database. + @raise ValueError: If the database is not of the right type. + """ + if not self.filename: + raise ValueError("Can only open on-disk databases") + self.db = anydbm.open(self.filename, "w") #raises anydbm.error + try: + if self.db["--Reserved--type"] != self.type: + raise ValueError("Not a %s database" % self.type) + except KeyError: + raise ValueError("Not a recognized database") + + def __getitem__(self, username): + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + valueStr = self.db[username] + finally: + self.lock.release() + + return self._getItem(username, valueStr) + + def __setitem__(self, username, value): + if self.db == None: + raise AssertionError("DB not open") + + valueStr = self._setItem(username, value) + + self.lock.acquire() + try: + self.db[username] = valueStr + if self.filename: + self.db.sync() + finally: + self.lock.release() + + def __delitem__(self, username): + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + del(self.db[username]) + if self.filename: + self.db.sync() + finally: + self.lock.release() + + def __contains__(self, username): + """Check if the database contains the specified username. + + @type username: str + @param username: The username to check for. + + @rtype: bool + @return: True if the database contains the username, False + otherwise. + + """ + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + return self.db.has_key(username) + finally: + self.lock.release() + + def check(self, username, param): + value = self.__getitem__(username) + return self._checkItem(value, username, param) + + def keys(self): + """Return a list of usernames in the database. + + @rtype: list + @return: The usernames in the database. + """ + if self.db == None: + raise AssertionError("DB not open") + + self.lock.acquire() + try: + usernames = self.db.keys() + finally: + self.lock.release() + usernames = [u for u in usernames if not u.startswith("--Reserved--")] return usernames \ No newline at end of file diff --git a/gdata/tlslite/Checker.py b/src/gdata/tlslite/Checker.py similarity index 100% rename from gdata/tlslite/Checker.py rename to src/gdata/tlslite/Checker.py diff --git a/gdata/tlslite/FileObject.py b/src/gdata/tlslite/FileObject.py similarity index 100% rename from gdata/tlslite/FileObject.py rename to src/gdata/tlslite/FileObject.py diff --git a/gdata/tlslite/HandshakeSettings.py b/src/gdata/tlslite/HandshakeSettings.py similarity index 100% rename from gdata/tlslite/HandshakeSettings.py rename to src/gdata/tlslite/HandshakeSettings.py diff --git a/gdata/tlslite/Session.py b/src/gdata/tlslite/Session.py similarity index 100% rename from gdata/tlslite/Session.py rename to src/gdata/tlslite/Session.py diff --git a/gdata/tlslite/SessionCache.py b/src/gdata/tlslite/SessionCache.py similarity index 100% rename from gdata/tlslite/SessionCache.py rename to src/gdata/tlslite/SessionCache.py diff --git a/gdata/tlslite/SharedKeyDB.py b/src/gdata/tlslite/SharedKeyDB.py similarity index 100% rename from gdata/tlslite/SharedKeyDB.py rename to src/gdata/tlslite/SharedKeyDB.py diff --git a/gdata/tlslite/TLSConnection.py b/src/gdata/tlslite/TLSConnection.py similarity index 100% rename from gdata/tlslite/TLSConnection.py rename to src/gdata/tlslite/TLSConnection.py diff --git a/gdata/tlslite/TLSRecordLayer.py b/src/gdata/tlslite/TLSRecordLayer.py similarity index 100% rename from gdata/tlslite/TLSRecordLayer.py rename to src/gdata/tlslite/TLSRecordLayer.py diff --git a/gdata/tlslite/VerifierDB.py b/src/gdata/tlslite/VerifierDB.py similarity index 100% rename from gdata/tlslite/VerifierDB.py rename to src/gdata/tlslite/VerifierDB.py diff --git a/gdata/tlslite/X509.py b/src/gdata/tlslite/X509.py similarity index 100% rename from gdata/tlslite/X509.py rename to src/gdata/tlslite/X509.py diff --git a/gdata/tlslite/X509CertChain.py b/src/gdata/tlslite/X509CertChain.py similarity index 97% rename from gdata/tlslite/X509CertChain.py rename to src/gdata/tlslite/X509CertChain.py index d5f0b4d4..6bb503e4 100644 --- a/gdata/tlslite/X509CertChain.py +++ b/src/gdata/tlslite/X509CertChain.py @@ -1,181 +1,181 @@ -"""Class representing an X.509 certificate chain.""" - -from utils import cryptomath - -class X509CertChain: - """This class represents a chain of X.509 certificates. - - @type x509List: list - @ivar x509List: A list of L{tlslite.X509.X509} instances, - starting with the end-entity certificate and with every - subsequent certificate certifying the previous. - """ - - def __init__(self, x509List=None): - """Create a new X509CertChain. - - @type x509List: list - @param x509List: A list of L{tlslite.X509.X509} instances, - starting with the end-entity certificate and with every - subsequent certificate certifying the previous. - """ - if x509List: - self.x509List = x509List - else: - self.x509List = [] - - def getNumCerts(self): - """Get the number of certificates in this chain. - - @rtype: int - """ - return len(self.x509List) - - def getEndEntityPublicKey(self): - """Get the public key from the end-entity certificate. - - @rtype: L{tlslite.utils.RSAKey.RSAKey} - """ - if self.getNumCerts() == 0: - raise AssertionError() - return self.x509List[0].publicKey - - def getFingerprint(self): - """Get the hex-encoded fingerprint of the end-entity certificate. - - @rtype: str - @return: A hex-encoded fingerprint. - """ - if self.getNumCerts() == 0: - raise AssertionError() - return self.x509List[0].getFingerprint() - - def getCommonName(self): - """Get the Subject's Common Name from the end-entity certificate. - - The cryptlib_py module must be installed in order to use this - function. - - @rtype: str or None - @return: The CN component of the certificate's subject DN, if - present. - """ - if self.getNumCerts() == 0: - raise AssertionError() - return self.x509List[0].getCommonName() - - def validate(self, x509TrustList): - """Check the validity of the certificate chain. - - This checks that every certificate in the chain validates with - the subsequent one, until some certificate validates with (or - is identical to) one of the passed-in root certificates. - - The cryptlib_py module must be installed in order to use this - function. - - @type x509TrustList: list of L{tlslite.X509.X509} - @param x509TrustList: A list of trusted root certificates. The - certificate chain must extend to one of these certificates to - be considered valid. - """ - - import cryptlib_py - c1 = None - c2 = None - lastC = None - rootC = None - - try: - rootFingerprints = [c.getFingerprint() for c in x509TrustList] - - #Check that every certificate in the chain validates with the - #next one - for cert1, cert2 in zip(self.x509List, self.x509List[1:]): - - #If we come upon a root certificate, we're done. - if cert1.getFingerprint() in rootFingerprints: - return True - - c1 = cryptlib_py.cryptImportCert(cert1.writeBytes(), - cryptlib_py.CRYPT_UNUSED) - c2 = cryptlib_py.cryptImportCert(cert2.writeBytes(), - cryptlib_py.CRYPT_UNUSED) - try: - cryptlib_py.cryptCheckCert(c1, c2) - except: - return False - cryptlib_py.cryptDestroyCert(c1) - c1 = None - cryptlib_py.cryptDestroyCert(c2) - c2 = None - - #If the last certificate is one of the root certificates, we're - #done. - if self.x509List[-1].getFingerprint() in rootFingerprints: - return True - - #Otherwise, find a root certificate that the last certificate - #chains to, and validate them. - lastC = cryptlib_py.cryptImportCert(self.x509List[-1].writeBytes(), - cryptlib_py.CRYPT_UNUSED) - for rootCert in x509TrustList: - rootC = cryptlib_py.cryptImportCert(rootCert.writeBytes(), - cryptlib_py.CRYPT_UNUSED) - if self._checkChaining(lastC, rootC): - try: - cryptlib_py.cryptCheckCert(lastC, rootC) - return True - except: - return False - return False - finally: - if not (c1 is None): - cryptlib_py.cryptDestroyCert(c1) - if not (c2 is None): - cryptlib_py.cryptDestroyCert(c2) - if not (lastC is None): - cryptlib_py.cryptDestroyCert(lastC) - if not (rootC is None): - cryptlib_py.cryptDestroyCert(rootC) - - - - def _checkChaining(self, lastC, rootC): - import cryptlib_py - import array - def compareNames(name): - try: - length = cryptlib_py.cryptGetAttributeString(lastC, name, None) - lastName = array.array('B', [0] * length) - cryptlib_py.cryptGetAttributeString(lastC, name, lastName) - lastName = lastName.tostring() - except cryptlib_py.CryptException, e: - if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: - lastName = None - try: - length = cryptlib_py.cryptGetAttributeString(rootC, name, None) - rootName = array.array('B', [0] * length) - cryptlib_py.cryptGetAttributeString(rootC, name, rootName) - rootName = rootName.tostring() - except cryptlib_py.CryptException, e: - if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: - rootName = None - - return lastName == rootName - - cryptlib_py.cryptSetAttribute(lastC, - cryptlib_py.CRYPT_CERTINFO_ISSUERNAME, - cryptlib_py.CRYPT_UNUSED) - - if not compareNames(cryptlib_py.CRYPT_CERTINFO_COUNTRYNAME): - return False - if not compareNames(cryptlib_py.CRYPT_CERTINFO_LOCALITYNAME): - return False - if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONNAME): - return False - if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONALUNITNAME): - return False - if not compareNames(cryptlib_py.CRYPT_CERTINFO_COMMONNAME): - return False +"""Class representing an X.509 certificate chain.""" + +from utils import cryptomath + +class X509CertChain: + """This class represents a chain of X.509 certificates. + + @type x509List: list + @ivar x509List: A list of L{tlslite.X509.X509} instances, + starting with the end-entity certificate and with every + subsequent certificate certifying the previous. + """ + + def __init__(self, x509List=None): + """Create a new X509CertChain. + + @type x509List: list + @param x509List: A list of L{tlslite.X509.X509} instances, + starting with the end-entity certificate and with every + subsequent certificate certifying the previous. + """ + if x509List: + self.x509List = x509List + else: + self.x509List = [] + + def getNumCerts(self): + """Get the number of certificates in this chain. + + @rtype: int + """ + return len(self.x509List) + + def getEndEntityPublicKey(self): + """Get the public key from the end-entity certificate. + + @rtype: L{tlslite.utils.RSAKey.RSAKey} + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].publicKey + + def getFingerprint(self): + """Get the hex-encoded fingerprint of the end-entity certificate. + + @rtype: str + @return: A hex-encoded fingerprint. + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].getFingerprint() + + def getCommonName(self): + """Get the Subject's Common Name from the end-entity certificate. + + The cryptlib_py module must be installed in order to use this + function. + + @rtype: str or None + @return: The CN component of the certificate's subject DN, if + present. + """ + if self.getNumCerts() == 0: + raise AssertionError() + return self.x509List[0].getCommonName() + + def validate(self, x509TrustList): + """Check the validity of the certificate chain. + + This checks that every certificate in the chain validates with + the subsequent one, until some certificate validates with (or + is identical to) one of the passed-in root certificates. + + The cryptlib_py module must be installed in order to use this + function. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + certificate chain must extend to one of these certificates to + be considered valid. + """ + + import cryptlib_py + c1 = None + c2 = None + lastC = None + rootC = None + + try: + rootFingerprints = [c.getFingerprint() for c in x509TrustList] + + #Check that every certificate in the chain validates with the + #next one + for cert1, cert2 in zip(self.x509List, self.x509List[1:]): + + #If we come upon a root certificate, we're done. + if cert1.getFingerprint() in rootFingerprints: + return True + + c1 = cryptlib_py.cryptImportCert(cert1.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + c2 = cryptlib_py.cryptImportCert(cert2.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + try: + cryptlib_py.cryptCheckCert(c1, c2) + except: + return False + cryptlib_py.cryptDestroyCert(c1) + c1 = None + cryptlib_py.cryptDestroyCert(c2) + c2 = None + + #If the last certificate is one of the root certificates, we're + #done. + if self.x509List[-1].getFingerprint() in rootFingerprints: + return True + + #Otherwise, find a root certificate that the last certificate + #chains to, and validate them. + lastC = cryptlib_py.cryptImportCert(self.x509List[-1].writeBytes(), + cryptlib_py.CRYPT_UNUSED) + for rootCert in x509TrustList: + rootC = cryptlib_py.cryptImportCert(rootCert.writeBytes(), + cryptlib_py.CRYPT_UNUSED) + if self._checkChaining(lastC, rootC): + try: + cryptlib_py.cryptCheckCert(lastC, rootC) + return True + except: + return False + return False + finally: + if not (c1 is None): + cryptlib_py.cryptDestroyCert(c1) + if not (c2 is None): + cryptlib_py.cryptDestroyCert(c2) + if not (lastC is None): + cryptlib_py.cryptDestroyCert(lastC) + if not (rootC is None): + cryptlib_py.cryptDestroyCert(rootC) + + + + def _checkChaining(self, lastC, rootC): + import cryptlib_py + import array + def compareNames(name): + try: + length = cryptlib_py.cryptGetAttributeString(lastC, name, None) + lastName = array.array('B', [0] * length) + cryptlib_py.cryptGetAttributeString(lastC, name, lastName) + lastName = lastName.tostring() + except cryptlib_py.CryptException, e: + if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: + lastName = None + try: + length = cryptlib_py.cryptGetAttributeString(rootC, name, None) + rootName = array.array('B', [0] * length) + cryptlib_py.cryptGetAttributeString(rootC, name, rootName) + rootName = rootName.tostring() + except cryptlib_py.CryptException, e: + if e[0] == cryptlib_py.CRYPT_ERROR_NOTFOUND: + rootName = None + + return lastName == rootName + + cryptlib_py.cryptSetAttribute(lastC, + cryptlib_py.CRYPT_CERTINFO_ISSUERNAME, + cryptlib_py.CRYPT_UNUSED) + + if not compareNames(cryptlib_py.CRYPT_CERTINFO_COUNTRYNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_LOCALITYNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_ORGANIZATIONALUNITNAME): + return False + if not compareNames(cryptlib_py.CRYPT_CERTINFO_COMMONNAME): + return False return True \ No newline at end of file diff --git a/gdata/tlslite/__init__.py b/src/gdata/tlslite/__init__.py similarity index 100% rename from gdata/tlslite/__init__.py rename to src/gdata/tlslite/__init__.py diff --git a/gdata/tlslite/api.py b/src/gdata/tlslite/api.py similarity index 100% rename from gdata/tlslite/api.py rename to src/gdata/tlslite/api.py diff --git a/gdata/tlslite/constants.py b/src/gdata/tlslite/constants.py similarity index 100% rename from gdata/tlslite/constants.py rename to src/gdata/tlslite/constants.py diff --git a/gdata/tlslite/errors.py b/src/gdata/tlslite/errors.py similarity index 100% rename from gdata/tlslite/errors.py rename to src/gdata/tlslite/errors.py diff --git a/gdata/tlslite/integration/AsyncStateMachine.py b/src/gdata/tlslite/integration/AsyncStateMachine.py similarity index 96% rename from gdata/tlslite/integration/AsyncStateMachine.py rename to src/gdata/tlslite/integration/AsyncStateMachine.py index abed6043..01c7efe6 100644 --- a/gdata/tlslite/integration/AsyncStateMachine.py +++ b/src/gdata/tlslite/integration/AsyncStateMachine.py @@ -1,235 +1,235 @@ -""" -A state machine for using TLS Lite with asynchronous I/O. -""" - -class AsyncStateMachine: - """ - This is an abstract class that's used to integrate TLS Lite with - asyncore and Twisted. - - This class signals wantsReadsEvent() and wantsWriteEvent(). When - the underlying socket has become readable or writeable, the event - should be passed to this class by calling inReadEvent() or - inWriteEvent(). This class will then try to read or write through - the socket, and will update its state appropriately. - - This class will forward higher-level events to its subclass. For - example, when a complete TLS record has been received, - outReadEvent() will be called with the decrypted data. - """ - - def __init__(self): - self._clear() - - def _clear(self): - #These store the various asynchronous operations (i.e. - #generators). Only one of them, at most, is ever active at a - #time. - self.handshaker = None - self.closer = None - self.reader = None - self.writer = None - - #This stores the result from the last call to the - #currently active operation. If 0 it indicates that the - #operation wants to read, if 1 it indicates that the - #operation wants to write. If None, there is no active - #operation. - self.result = None - - def _checkAssert(self, maxActive=1): - #This checks that only one operation, at most, is - #active, and that self.result is set appropriately. - activeOps = 0 - if self.handshaker: - activeOps += 1 - if self.closer: - activeOps += 1 - if self.reader: - activeOps += 1 - if self.writer: - activeOps += 1 - - if self.result == None: - if activeOps != 0: - raise AssertionError() - elif self.result in (0,1): - if activeOps != 1: - raise AssertionError() - else: - raise AssertionError() - if activeOps > maxActive: - raise AssertionError() - - def wantsReadEvent(self): - """If the state machine wants to read. - - If an operation is active, this returns whether or not the - operation wants to read from the socket. If an operation is - not active, this returns None. - - @rtype: bool or None - @return: If the state machine wants to read. - """ - if self.result != None: - return self.result == 0 - return None - - def wantsWriteEvent(self): - """If the state machine wants to write. - - If an operation is active, this returns whether or not the - operation wants to write to the socket. If an operation is - not active, this returns None. - - @rtype: bool or None - @return: If the state machine wants to write. - """ - if self.result != None: - return self.result == 1 - return None - - def outConnectEvent(self): - """Called when a handshake operation completes. - - May be overridden in subclass. - """ - pass - - def outCloseEvent(self): - """Called when a close operation completes. - - May be overridden in subclass. - """ - pass - - def outReadEvent(self, readBuffer): - """Called when a read operation completes. - - May be overridden in subclass.""" - pass - - def outWriteEvent(self): - """Called when a write operation completes. - - May be overridden in subclass.""" - pass - - def inReadEvent(self): - """Tell the state machine it can read from the socket.""" - try: - self._checkAssert() - if self.handshaker: - self._doHandshakeOp() - elif self.closer: - self._doCloseOp() - elif self.reader: - self._doReadOp() - elif self.writer: - self._doWriteOp() - else: - self.reader = self.tlsConnection.readAsync(16384) - self._doReadOp() - except: - self._clear() - raise - - def inWriteEvent(self): - """Tell the state machine it can write to the socket.""" - try: - self._checkAssert() - if self.handshaker: - self._doHandshakeOp() - elif self.closer: - self._doCloseOp() - elif self.reader: - self._doReadOp() - elif self.writer: - self._doWriteOp() - else: - self.outWriteEvent() - except: - self._clear() - raise - - def _doHandshakeOp(self): - try: - self.result = self.handshaker.next() - except StopIteration: - self.handshaker = None - self.result = None - self.outConnectEvent() - - def _doCloseOp(self): - try: - self.result = self.closer.next() - except StopIteration: - self.closer = None - self.result = None - self.outCloseEvent() - - def _doReadOp(self): - self.result = self.reader.next() - if not self.result in (0,1): - readBuffer = self.result - self.reader = None - self.result = None - self.outReadEvent(readBuffer) - - def _doWriteOp(self): - try: - self.result = self.writer.next() - except StopIteration: - self.writer = None - self.result = None - - def setHandshakeOp(self, handshaker): - """Start a handshake operation. - - @type handshaker: generator - @param handshaker: A generator created by using one of the - asynchronous handshake functions (i.e. handshakeServerAsync, or - handshakeClientxxx(..., async=True). - """ - try: - self._checkAssert(0) - self.handshaker = handshaker - self._doHandshakeOp() - except: - self._clear() - raise - - def setServerHandshakeOp(self, **args): - """Start a handshake operation. - - The arguments passed to this function will be forwarded to - L{tlslite.TLSConnection.TLSConnection.handshakeServerAsync}. - """ - handshaker = self.tlsConnection.handshakeServerAsync(**args) - self.setHandshakeOp(handshaker) - - def setCloseOp(self): - """Start a close operation. - """ - try: - self._checkAssert(0) - self.closer = self.tlsConnection.closeAsync() - self._doCloseOp() - except: - self._clear() - raise - - def setWriteOp(self, writeBuffer): - """Start a write operation. - - @type writeBuffer: str - @param writeBuffer: The string to transmit. - """ - try: - self._checkAssert(0) - self.writer = self.tlsConnection.writeAsync(writeBuffer) - self._doWriteOp() - except: - self._clear() - raise - +""" +A state machine for using TLS Lite with asynchronous I/O. +""" + +class AsyncStateMachine: + """ + This is an abstract class that's used to integrate TLS Lite with + asyncore and Twisted. + + This class signals wantsReadsEvent() and wantsWriteEvent(). When + the underlying socket has become readable or writeable, the event + should be passed to this class by calling inReadEvent() or + inWriteEvent(). This class will then try to read or write through + the socket, and will update its state appropriately. + + This class will forward higher-level events to its subclass. For + example, when a complete TLS record has been received, + outReadEvent() will be called with the decrypted data. + """ + + def __init__(self): + self._clear() + + def _clear(self): + #These store the various asynchronous operations (i.e. + #generators). Only one of them, at most, is ever active at a + #time. + self.handshaker = None + self.closer = None + self.reader = None + self.writer = None + + #This stores the result from the last call to the + #currently active operation. If 0 it indicates that the + #operation wants to read, if 1 it indicates that the + #operation wants to write. If None, there is no active + #operation. + self.result = None + + def _checkAssert(self, maxActive=1): + #This checks that only one operation, at most, is + #active, and that self.result is set appropriately. + activeOps = 0 + if self.handshaker: + activeOps += 1 + if self.closer: + activeOps += 1 + if self.reader: + activeOps += 1 + if self.writer: + activeOps += 1 + + if self.result == None: + if activeOps != 0: + raise AssertionError() + elif self.result in (0,1): + if activeOps != 1: + raise AssertionError() + else: + raise AssertionError() + if activeOps > maxActive: + raise AssertionError() + + def wantsReadEvent(self): + """If the state machine wants to read. + + If an operation is active, this returns whether or not the + operation wants to read from the socket. If an operation is + not active, this returns None. + + @rtype: bool or None + @return: If the state machine wants to read. + """ + if self.result != None: + return self.result == 0 + return None + + def wantsWriteEvent(self): + """If the state machine wants to write. + + If an operation is active, this returns whether or not the + operation wants to write to the socket. If an operation is + not active, this returns None. + + @rtype: bool or None + @return: If the state machine wants to write. + """ + if self.result != None: + return self.result == 1 + return None + + def outConnectEvent(self): + """Called when a handshake operation completes. + + May be overridden in subclass. + """ + pass + + def outCloseEvent(self): + """Called when a close operation completes. + + May be overridden in subclass. + """ + pass + + def outReadEvent(self, readBuffer): + """Called when a read operation completes. + + May be overridden in subclass.""" + pass + + def outWriteEvent(self): + """Called when a write operation completes. + + May be overridden in subclass.""" + pass + + def inReadEvent(self): + """Tell the state machine it can read from the socket.""" + try: + self._checkAssert() + if self.handshaker: + self._doHandshakeOp() + elif self.closer: + self._doCloseOp() + elif self.reader: + self._doReadOp() + elif self.writer: + self._doWriteOp() + else: + self.reader = self.tlsConnection.readAsync(16384) + self._doReadOp() + except: + self._clear() + raise + + def inWriteEvent(self): + """Tell the state machine it can write to the socket.""" + try: + self._checkAssert() + if self.handshaker: + self._doHandshakeOp() + elif self.closer: + self._doCloseOp() + elif self.reader: + self._doReadOp() + elif self.writer: + self._doWriteOp() + else: + self.outWriteEvent() + except: + self._clear() + raise + + def _doHandshakeOp(self): + try: + self.result = self.handshaker.next() + except StopIteration: + self.handshaker = None + self.result = None + self.outConnectEvent() + + def _doCloseOp(self): + try: + self.result = self.closer.next() + except StopIteration: + self.closer = None + self.result = None + self.outCloseEvent() + + def _doReadOp(self): + self.result = self.reader.next() + if not self.result in (0,1): + readBuffer = self.result + self.reader = None + self.result = None + self.outReadEvent(readBuffer) + + def _doWriteOp(self): + try: + self.result = self.writer.next() + except StopIteration: + self.writer = None + self.result = None + + def setHandshakeOp(self, handshaker): + """Start a handshake operation. + + @type handshaker: generator + @param handshaker: A generator created by using one of the + asynchronous handshake functions (i.e. handshakeServerAsync, or + handshakeClientxxx(..., async=True). + """ + try: + self._checkAssert(0) + self.handshaker = handshaker + self._doHandshakeOp() + except: + self._clear() + raise + + def setServerHandshakeOp(self, **args): + """Start a handshake operation. + + The arguments passed to this function will be forwarded to + L{tlslite.TLSConnection.TLSConnection.handshakeServerAsync}. + """ + handshaker = self.tlsConnection.handshakeServerAsync(**args) + self.setHandshakeOp(handshaker) + + def setCloseOp(self): + """Start a close operation. + """ + try: + self._checkAssert(0) + self.closer = self.tlsConnection.closeAsync() + self._doCloseOp() + except: + self._clear() + raise + + def setWriteOp(self, writeBuffer): + """Start a write operation. + + @type writeBuffer: str + @param writeBuffer: The string to transmit. + """ + try: + self._checkAssert(0) + self.writer = self.tlsConnection.writeAsync(writeBuffer) + self._doWriteOp() + except: + self._clear() + raise + diff --git a/gdata/tlslite/integration/ClientHelper.py b/src/gdata/tlslite/integration/ClientHelper.py similarity index 97% rename from gdata/tlslite/integration/ClientHelper.py rename to src/gdata/tlslite/integration/ClientHelper.py index 58e0152f..a3760b51 100644 --- a/gdata/tlslite/integration/ClientHelper.py +++ b/src/gdata/tlslite/integration/ClientHelper.py @@ -1,163 +1,163 @@ -""" -A helper class for using TLS Lite with stdlib clients -(httplib, xmlrpclib, imaplib, poplib). -""" - -from gdata.tlslite.Checker import Checker - -class ClientHelper: - """This is a helper class used to integrate TLS Lite with various - TLS clients (e.g. poplib, smtplib, httplib, etc.)""" - - def __init__(self, - username=None, password=None, sharedKey=None, - certChain=None, privateKey=None, - cryptoID=None, protocol=None, - x509Fingerprint=None, - x509TrustList=None, x509CommonName=None, - settings = None): - """ - For client authentication, use one of these argument - combinations: - - username, password (SRP) - - username, sharedKey (shared-key) - - certChain, privateKey (certificate) - - For server authentication, you can either rely on the - implicit mutual authentication performed by SRP or - shared-keys, or you can do certificate-based server - authentication with one of these argument combinations: - - cryptoID[, protocol] (requires cryptoIDlib) - - x509Fingerprint - - x509TrustList[, x509CommonName] (requires cryptlib_py) - - Certificate-based server authentication is compatible with - SRP or certificate-based client authentication. It is - not compatible with shared-keys. - - The constructor does not perform the TLS handshake itself, but - simply stores these arguments for later. The handshake is - performed only when this class needs to connect with the - server. Then you should be prepared to handle TLS-specific - exceptions. See the client handshake functions in - L{tlslite.TLSConnection.TLSConnection} for details on which - exceptions might be raised. - - @type username: str - @param username: SRP or shared-key username. Requires the - 'password' or 'sharedKey' argument. - - @type password: str - @param password: SRP password for mutual authentication. - Requires the 'username' argument. - - @type sharedKey: str - @param sharedKey: Shared key for mutual authentication. - Requires the 'username' argument. - - @type certChain: L{tlslite.X509CertChain.X509CertChain} or - L{cryptoIDlib.CertChain.CertChain} - @param certChain: Certificate chain for client authentication. - Requires the 'privateKey' argument. Excludes the SRP or - shared-key related arguments. - - @type privateKey: L{tlslite.utils.RSAKey.RSAKey} - @param privateKey: Private key for client authentication. - Requires the 'certChain' argument. Excludes the SRP or - shared-key related arguments. - - @type cryptoID: str - @param cryptoID: cryptoID for server authentication. Mutually - exclusive with the 'x509...' arguments. - - @type protocol: str - @param protocol: cryptoID protocol URI for server - authentication. Requires the 'cryptoID' argument. - - @type x509Fingerprint: str - @param x509Fingerprint: Hex-encoded X.509 fingerprint for - server authentication. Mutually exclusive with the 'cryptoID' - and 'x509TrustList' arguments. - - @type x509TrustList: list of L{tlslite.X509.X509} - @param x509TrustList: A list of trusted root certificates. The - other party must present a certificate chain which extends to - one of these root certificates. The cryptlib_py module must be - installed to use this parameter. Mutually exclusive with the - 'cryptoID' and 'x509Fingerprint' arguments. - - @type x509CommonName: str - @param x509CommonName: The end-entity certificate's 'CN' field - must match this value. For a web server, this is typically a - server name such as 'www.amazon.com'. Mutually exclusive with - the 'cryptoID' and 'x509Fingerprint' arguments. Requires the - 'x509TrustList' argument. - - @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} - @param settings: Various settings which can be used to control - the ciphersuites, certificate types, and SSL/TLS versions - offered by the client. - """ - - self.username = None - self.password = None - self.sharedKey = None - self.certChain = None - self.privateKey = None - self.checker = None - - #SRP Authentication - if username and password and not \ - (sharedKey or certChain or privateKey): - self.username = username - self.password = password - - #Shared Key Authentication - elif username and sharedKey and not \ - (password or certChain or privateKey): - self.username = username - self.sharedKey = sharedKey - - #Certificate Chain Authentication - elif certChain and privateKey and not \ - (username or password or sharedKey): - self.certChain = certChain - self.privateKey = privateKey - - #No Authentication - elif not password and not username and not \ - sharedKey and not certChain and not privateKey: - pass - - else: - raise ValueError("Bad parameters") - - #Authenticate the server based on its cryptoID or fingerprint - if sharedKey and (cryptoID or protocol or x509Fingerprint): - raise ValueError("Can't use shared keys with other forms of"\ - "authentication") - - self.checker = Checker(cryptoID, protocol, x509Fingerprint, - x509TrustList, x509CommonName) - self.settings = settings - - self.tlsSession = None - - def _handshake(self, tlsConnection): - if self.username and self.password: - tlsConnection.handshakeClientSRP(username=self.username, - password=self.password, - checker=self.checker, - settings=self.settings, - session=self.tlsSession) - elif self.username and self.sharedKey: - tlsConnection.handshakeClientSharedKey(username=self.username, - sharedKey=self.sharedKey, - settings=self.settings) - else: - tlsConnection.handshakeClientCert(certChain=self.certChain, - privateKey=self.privateKey, - checker=self.checker, - settings=self.settings, - session=self.tlsSession) - self.tlsSession = tlsConnection.session +""" +A helper class for using TLS Lite with stdlib clients +(httplib, xmlrpclib, imaplib, poplib). +""" + +from gdata.tlslite.Checker import Checker + +class ClientHelper: + """This is a helper class used to integrate TLS Lite with various + TLS clients (e.g. poplib, smtplib, httplib, etc.)""" + + def __init__(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings = None): + """ + For client authentication, use one of these argument + combinations: + - username, password (SRP) + - username, sharedKey (shared-key) + - certChain, privateKey (certificate) + + For server authentication, you can either rely on the + implicit mutual authentication performed by SRP or + shared-keys, or you can do certificate-based server + authentication with one of these argument combinations: + - cryptoID[, protocol] (requires cryptoIDlib) + - x509Fingerprint + - x509TrustList[, x509CommonName] (requires cryptlib_py) + + Certificate-based server authentication is compatible with + SRP or certificate-based client authentication. It is + not compatible with shared-keys. + + The constructor does not perform the TLS handshake itself, but + simply stores these arguments for later. The handshake is + performed only when this class needs to connect with the + server. Then you should be prepared to handle TLS-specific + exceptions. See the client handshake functions in + L{tlslite.TLSConnection.TLSConnection} for details on which + exceptions might be raised. + + @type username: str + @param username: SRP or shared-key username. Requires the + 'password' or 'sharedKey' argument. + + @type password: str + @param password: SRP password for mutual authentication. + Requires the 'username' argument. + + @type sharedKey: str + @param sharedKey: Shared key for mutual authentication. + Requires the 'username' argument. + + @type certChain: L{tlslite.X509CertChain.X509CertChain} or + L{cryptoIDlib.CertChain.CertChain} + @param certChain: Certificate chain for client authentication. + Requires the 'privateKey' argument. Excludes the SRP or + shared-key related arguments. + + @type privateKey: L{tlslite.utils.RSAKey.RSAKey} + @param privateKey: Private key for client authentication. + Requires the 'certChain' argument. Excludes the SRP or + shared-key related arguments. + + @type cryptoID: str + @param cryptoID: cryptoID for server authentication. Mutually + exclusive with the 'x509...' arguments. + + @type protocol: str + @param protocol: cryptoID protocol URI for server + authentication. Requires the 'cryptoID' argument. + + @type x509Fingerprint: str + @param x509Fingerprint: Hex-encoded X.509 fingerprint for + server authentication. Mutually exclusive with the 'cryptoID' + and 'x509TrustList' arguments. + + @type x509TrustList: list of L{tlslite.X509.X509} + @param x509TrustList: A list of trusted root certificates. The + other party must present a certificate chain which extends to + one of these root certificates. The cryptlib_py module must be + installed to use this parameter. Mutually exclusive with the + 'cryptoID' and 'x509Fingerprint' arguments. + + @type x509CommonName: str + @param x509CommonName: The end-entity certificate's 'CN' field + must match this value. For a web server, this is typically a + server name such as 'www.amazon.com'. Mutually exclusive with + the 'cryptoID' and 'x509Fingerprint' arguments. Requires the + 'x509TrustList' argument. + + @type settings: L{tlslite.HandshakeSettings.HandshakeSettings} + @param settings: Various settings which can be used to control + the ciphersuites, certificate types, and SSL/TLS versions + offered by the client. + """ + + self.username = None + self.password = None + self.sharedKey = None + self.certChain = None + self.privateKey = None + self.checker = None + + #SRP Authentication + if username and password and not \ + (sharedKey or certChain or privateKey): + self.username = username + self.password = password + + #Shared Key Authentication + elif username and sharedKey and not \ + (password or certChain or privateKey): + self.username = username + self.sharedKey = sharedKey + + #Certificate Chain Authentication + elif certChain and privateKey and not \ + (username or password or sharedKey): + self.certChain = certChain + self.privateKey = privateKey + + #No Authentication + elif not password and not username and not \ + sharedKey and not certChain and not privateKey: + pass + + else: + raise ValueError("Bad parameters") + + #Authenticate the server based on its cryptoID or fingerprint + if sharedKey and (cryptoID or protocol or x509Fingerprint): + raise ValueError("Can't use shared keys with other forms of"\ + "authentication") + + self.checker = Checker(cryptoID, protocol, x509Fingerprint, + x509TrustList, x509CommonName) + self.settings = settings + + self.tlsSession = None + + def _handshake(self, tlsConnection): + if self.username and self.password: + tlsConnection.handshakeClientSRP(username=self.username, + password=self.password, + checker=self.checker, + settings=self.settings, + session=self.tlsSession) + elif self.username and self.sharedKey: + tlsConnection.handshakeClientSharedKey(username=self.username, + sharedKey=self.sharedKey, + settings=self.settings) + else: + tlsConnection.handshakeClientCert(certChain=self.certChain, + privateKey=self.privateKey, + checker=self.checker, + settings=self.settings, + session=self.tlsSession) + self.tlsSession = tlsConnection.session diff --git a/gdata/tlslite/integration/HTTPTLSConnection.py b/src/gdata/tlslite/integration/HTTPTLSConnection.py similarity index 100% rename from gdata/tlslite/integration/HTTPTLSConnection.py rename to src/gdata/tlslite/integration/HTTPTLSConnection.py diff --git a/gdata/tlslite/integration/IMAP4_TLS.py b/src/gdata/tlslite/integration/IMAP4_TLS.py similarity index 100% rename from gdata/tlslite/integration/IMAP4_TLS.py rename to src/gdata/tlslite/integration/IMAP4_TLS.py diff --git a/gdata/tlslite/integration/IntegrationHelper.py b/src/gdata/tlslite/integration/IntegrationHelper.py similarity index 95% rename from gdata/tlslite/integration/IntegrationHelper.py rename to src/gdata/tlslite/integration/IntegrationHelper.py index af5193b4..ca81a486 100644 --- a/gdata/tlslite/integration/IntegrationHelper.py +++ b/src/gdata/tlslite/integration/IntegrationHelper.py @@ -1,52 +1,52 @@ - -class IntegrationHelper: - - def __init__(self, - username=None, password=None, sharedKey=None, - certChain=None, privateKey=None, - cryptoID=None, protocol=None, - x509Fingerprint=None, - x509TrustList=None, x509CommonName=None, - settings = None): - - self.username = None - self.password = None - self.sharedKey = None - self.certChain = None - self.privateKey = None - self.checker = None - - #SRP Authentication - if username and password and not \ - (sharedKey or certChain or privateKey): - self.username = username - self.password = password - - #Shared Key Authentication - elif username and sharedKey and not \ - (password or certChain or privateKey): - self.username = username - self.sharedKey = sharedKey - - #Certificate Chain Authentication - elif certChain and privateKey and not \ - (username or password or sharedKey): - self.certChain = certChain - self.privateKey = privateKey - - #No Authentication - elif not password and not username and not \ - sharedKey and not certChain and not privateKey: - pass - - else: - raise ValueError("Bad parameters") - - #Authenticate the server based on its cryptoID or fingerprint - if sharedKey and (cryptoID or protocol or x509Fingerprint): - raise ValueError("Can't use shared keys with other forms of"\ - "authentication") - - self.checker = Checker(cryptoID, protocol, x509Fingerprint, - x509TrustList, x509CommonName) + +class IntegrationHelper: + + def __init__(self, + username=None, password=None, sharedKey=None, + certChain=None, privateKey=None, + cryptoID=None, protocol=None, + x509Fingerprint=None, + x509TrustList=None, x509CommonName=None, + settings = None): + + self.username = None + self.password = None + self.sharedKey = None + self.certChain = None + self.privateKey = None + self.checker = None + + #SRP Authentication + if username and password and not \ + (sharedKey or certChain or privateKey): + self.username = username + self.password = password + + #Shared Key Authentication + elif username and sharedKey and not \ + (password or certChain or privateKey): + self.username = username + self.sharedKey = sharedKey + + #Certificate Chain Authentication + elif certChain and privateKey and not \ + (username or password or sharedKey): + self.certChain = certChain + self.privateKey = privateKey + + #No Authentication + elif not password and not username and not \ + sharedKey and not certChain and not privateKey: + pass + + else: + raise ValueError("Bad parameters") + + #Authenticate the server based on its cryptoID or fingerprint + if sharedKey and (cryptoID or protocol or x509Fingerprint): + raise ValueError("Can't use shared keys with other forms of"\ + "authentication") + + self.checker = Checker(cryptoID, protocol, x509Fingerprint, + x509TrustList, x509CommonName) self.settings = settings \ No newline at end of file diff --git a/gdata/tlslite/integration/POP3_TLS.py b/src/gdata/tlslite/integration/POP3_TLS.py similarity index 100% rename from gdata/tlslite/integration/POP3_TLS.py rename to src/gdata/tlslite/integration/POP3_TLS.py diff --git a/gdata/tlslite/integration/SMTP_TLS.py b/src/gdata/tlslite/integration/SMTP_TLS.py similarity index 100% rename from gdata/tlslite/integration/SMTP_TLS.py rename to src/gdata/tlslite/integration/SMTP_TLS.py diff --git a/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py b/src/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py similarity index 97% rename from gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py rename to src/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py index f732f62e..d2fe612f 100644 --- a/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py +++ b/src/gdata/tlslite/integration/TLSAsyncDispatcherMixIn.py @@ -1,139 +1,139 @@ -"""TLS Lite + asyncore.""" - - -import asyncore -from gdata.tlslite.TLSConnection import TLSConnection -from AsyncStateMachine import AsyncStateMachine - - -class TLSAsyncDispatcherMixIn(AsyncStateMachine): - """This class can be "mixed in" with an - L{asyncore.dispatcher} to add TLS support. - - This class essentially sits between the dispatcher and the select - loop, intercepting events and only calling the dispatcher when - applicable. - - In the case of handle_read(), a read operation will be activated, - and when it completes, the bytes will be placed in a buffer where - the dispatcher can retrieve them by calling recv(), and the - dispatcher's handle_read() will be called. - - In the case of handle_write(), the dispatcher's handle_write() will - be called, and when it calls send(), a write operation will be - activated. - - To use this class, you must combine it with an asyncore.dispatcher, - and pass in a handshake operation with setServerHandshakeOp(). - - Below is an example of using this class with medusa. This class is - mixed in with http_channel to create http_tls_channel. Note: - 1. the mix-in is listed first in the inheritance list - - 2. the input buffer size must be at least 16K, otherwise the - dispatcher might not read all the bytes from the TLS layer, - leaving some bytes in limbo. - - 3. IE seems to have a problem receiving a whole HTTP response in a - single TLS record, so HTML pages containing '\\r\\n\\r\\n' won't - be displayed on IE. - - Add the following text into 'start_medusa.py', in the 'HTTP Server' - section:: - - from tlslite.api import * - s = open("./serverX509Cert.pem").read() - x509 = X509() - x509.parse(s) - certChain = X509CertChain([x509]) - - s = open("./serverX509Key.pem").read() - privateKey = parsePEMKey(s, private=True) - - class http_tls_channel(TLSAsyncDispatcherMixIn, - http_server.http_channel): - ac_in_buffer_size = 16384 - - def __init__ (self, server, conn, addr): - http_server.http_channel.__init__(self, server, conn, addr) - TLSAsyncDispatcherMixIn.__init__(self, conn) - self.tlsConnection.ignoreAbruptClose = True - self.setServerHandshakeOp(certChain=certChain, - privateKey=privateKey) - - hs.channel_class = http_tls_channel - - If the TLS layer raises an exception, the exception will be caught - in asyncore.dispatcher, which will call close() on this class. The - TLS layer always closes the TLS connection before raising an - exception, so the close operation will complete right away, causing - asyncore.dispatcher.close() to be called, which closes the socket - and removes this instance from the asyncore loop. - - """ - - - def __init__(self, sock=None): - AsyncStateMachine.__init__(self) - - if sock: - self.tlsConnection = TLSConnection(sock) - - #Calculate the sibling I'm being mixed in with. - #This is necessary since we override functions - #like readable(), handle_read(), etc., but we - #also want to call the sibling's versions. - for cl in self.__class__.__bases__: - if cl != TLSAsyncDispatcherMixIn and cl != AsyncStateMachine: - self.siblingClass = cl - break - else: - raise AssertionError() - - def readable(self): - result = self.wantsReadEvent() - if result != None: - return result - return self.siblingClass.readable(self) - - def writable(self): - result = self.wantsWriteEvent() - if result != None: - return result - return self.siblingClass.writable(self) - - def handle_read(self): - self.inReadEvent() - - def handle_write(self): - self.inWriteEvent() - - def outConnectEvent(self): - self.siblingClass.handle_connect(self) - - def outCloseEvent(self): - asyncore.dispatcher.close(self) - - def outReadEvent(self, readBuffer): - self.readBuffer = readBuffer - self.siblingClass.handle_read(self) - - def outWriteEvent(self): - self.siblingClass.handle_write(self) - - def recv(self, bufferSize=16384): - if bufferSize < 16384 or self.readBuffer == None: - raise AssertionError() - returnValue = self.readBuffer - self.readBuffer = None - return returnValue - - def send(self, writeBuffer): - self.setWriteOp(writeBuffer) - return len(writeBuffer) - - def close(self): - if hasattr(self, "tlsConnection"): - self.setCloseOp() - else: - asyncore.dispatcher.close(self) +"""TLS Lite + asyncore.""" + + +import asyncore +from gdata.tlslite.TLSConnection import TLSConnection +from AsyncStateMachine import AsyncStateMachine + + +class TLSAsyncDispatcherMixIn(AsyncStateMachine): + """This class can be "mixed in" with an + L{asyncore.dispatcher} to add TLS support. + + This class essentially sits between the dispatcher and the select + loop, intercepting events and only calling the dispatcher when + applicable. + + In the case of handle_read(), a read operation will be activated, + and when it completes, the bytes will be placed in a buffer where + the dispatcher can retrieve them by calling recv(), and the + dispatcher's handle_read() will be called. + + In the case of handle_write(), the dispatcher's handle_write() will + be called, and when it calls send(), a write operation will be + activated. + + To use this class, you must combine it with an asyncore.dispatcher, + and pass in a handshake operation with setServerHandshakeOp(). + + Below is an example of using this class with medusa. This class is + mixed in with http_channel to create http_tls_channel. Note: + 1. the mix-in is listed first in the inheritance list + + 2. the input buffer size must be at least 16K, otherwise the + dispatcher might not read all the bytes from the TLS layer, + leaving some bytes in limbo. + + 3. IE seems to have a problem receiving a whole HTTP response in a + single TLS record, so HTML pages containing '\\r\\n\\r\\n' won't + be displayed on IE. + + Add the following text into 'start_medusa.py', in the 'HTTP Server' + section:: + + from tlslite.api import * + s = open("./serverX509Cert.pem").read() + x509 = X509() + x509.parse(s) + certChain = X509CertChain([x509]) + + s = open("./serverX509Key.pem").read() + privateKey = parsePEMKey(s, private=True) + + class http_tls_channel(TLSAsyncDispatcherMixIn, + http_server.http_channel): + ac_in_buffer_size = 16384 + + def __init__ (self, server, conn, addr): + http_server.http_channel.__init__(self, server, conn, addr) + TLSAsyncDispatcherMixIn.__init__(self, conn) + self.tlsConnection.ignoreAbruptClose = True + self.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey) + + hs.channel_class = http_tls_channel + + If the TLS layer raises an exception, the exception will be caught + in asyncore.dispatcher, which will call close() on this class. The + TLS layer always closes the TLS connection before raising an + exception, so the close operation will complete right away, causing + asyncore.dispatcher.close() to be called, which closes the socket + and removes this instance from the asyncore loop. + + """ + + + def __init__(self, sock=None): + AsyncStateMachine.__init__(self) + + if sock: + self.tlsConnection = TLSConnection(sock) + + #Calculate the sibling I'm being mixed in with. + #This is necessary since we override functions + #like readable(), handle_read(), etc., but we + #also want to call the sibling's versions. + for cl in self.__class__.__bases__: + if cl != TLSAsyncDispatcherMixIn and cl != AsyncStateMachine: + self.siblingClass = cl + break + else: + raise AssertionError() + + def readable(self): + result = self.wantsReadEvent() + if result != None: + return result + return self.siblingClass.readable(self) + + def writable(self): + result = self.wantsWriteEvent() + if result != None: + return result + return self.siblingClass.writable(self) + + def handle_read(self): + self.inReadEvent() + + def handle_write(self): + self.inWriteEvent() + + def outConnectEvent(self): + self.siblingClass.handle_connect(self) + + def outCloseEvent(self): + asyncore.dispatcher.close(self) + + def outReadEvent(self, readBuffer): + self.readBuffer = readBuffer + self.siblingClass.handle_read(self) + + def outWriteEvent(self): + self.siblingClass.handle_write(self) + + def recv(self, bufferSize=16384): + if bufferSize < 16384 or self.readBuffer == None: + raise AssertionError() + returnValue = self.readBuffer + self.readBuffer = None + return returnValue + + def send(self, writeBuffer): + self.setWriteOp(writeBuffer) + return len(writeBuffer) + + def close(self): + if hasattr(self, "tlsConnection"): + self.setCloseOp() + else: + asyncore.dispatcher.close(self) diff --git a/gdata/tlslite/integration/TLSSocketServerMixIn.py b/src/gdata/tlslite/integration/TLSSocketServerMixIn.py similarity index 100% rename from gdata/tlslite/integration/TLSSocketServerMixIn.py rename to src/gdata/tlslite/integration/TLSSocketServerMixIn.py diff --git a/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py b/src/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py similarity index 97% rename from gdata/tlslite/integration/TLSTwistedProtocolWrapper.py rename to src/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py index c88703ca..fba32be9 100644 --- a/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py +++ b/src/gdata/tlslite/integration/TLSTwistedProtocolWrapper.py @@ -1,196 +1,196 @@ -"""TLS Lite + Twisted.""" - -from twisted.protocols.policies import ProtocolWrapper, WrappingFactory -from twisted.python.failure import Failure - -from AsyncStateMachine import AsyncStateMachine -from gdata.tlslite.TLSConnection import TLSConnection -from gdata.tlslite.errors import * - -import socket -import errno - - -#The TLSConnection is created around a "fake socket" that -#plugs it into the underlying Twisted transport -class _FakeSocket: - def __init__(self, wrapper): - self.wrapper = wrapper - self.data = "" - - def send(self, data): - ProtocolWrapper.write(self.wrapper, data) - return len(data) - - def recv(self, numBytes): - if self.data == "": - raise socket.error, (errno.EWOULDBLOCK, "") - returnData = self.data[:numBytes] - self.data = self.data[numBytes:] - return returnData - -class TLSTwistedProtocolWrapper(ProtocolWrapper, AsyncStateMachine): - """This class can wrap Twisted protocols to add TLS support. - - Below is a complete example of using TLS Lite with a Twisted echo - server. - - There are two server implementations below. Echo is the original - protocol, which is oblivious to TLS. Echo1 subclasses Echo and - negotiates TLS when the client connects. Echo2 subclasses Echo and - negotiates TLS when the client sends "STARTTLS":: - - from twisted.internet.protocol import Protocol, Factory - from twisted.internet import reactor - from twisted.protocols.policies import WrappingFactory - from twisted.protocols.basic import LineReceiver - from twisted.python import log - from twisted.python.failure import Failure - import sys - from tlslite.api import * - - s = open("./serverX509Cert.pem").read() - x509 = X509() - x509.parse(s) - certChain = X509CertChain([x509]) - - s = open("./serverX509Key.pem").read() - privateKey = parsePEMKey(s, private=True) - - verifierDB = VerifierDB("verifierDB") - verifierDB.open() - - class Echo(LineReceiver): - def connectionMade(self): - self.transport.write("Welcome to the echo server!\\r\\n") - - def lineReceived(self, line): - self.transport.write(line + "\\r\\n") - - class Echo1(Echo): - def connectionMade(self): - if not self.transport.tlsStarted: - self.transport.setServerHandshakeOp(certChain=certChain, - privateKey=privateKey, - verifierDB=verifierDB) - else: - Echo.connectionMade(self) - - def connectionLost(self, reason): - pass #Handle any TLS exceptions here - - class Echo2(Echo): - def lineReceived(self, data): - if data == "STARTTLS": - self.transport.setServerHandshakeOp(certChain=certChain, - privateKey=privateKey, - verifierDB=verifierDB) - else: - Echo.lineReceived(self, data) - - def connectionLost(self, reason): - pass #Handle any TLS exceptions here - - factory = Factory() - factory.protocol = Echo1 - #factory.protocol = Echo2 - - wrappingFactory = WrappingFactory(factory) - wrappingFactory.protocol = TLSTwistedProtocolWrapper - - log.startLogging(sys.stdout) - reactor.listenTCP(1079, wrappingFactory) - reactor.run() - - This class works as follows: - - Data comes in and is given to the AsyncStateMachine for handling. - AsyncStateMachine will forward events to this class, and we'll - pass them on to the ProtocolHandler, which will proxy them to the - wrapped protocol. The wrapped protocol may then call back into - this class, and these calls will be proxied into the - AsyncStateMachine. - - The call graph looks like this: - - self.dataReceived - - AsyncStateMachine.inReadEvent - - self.out(Connect|Close|Read)Event - - ProtocolWrapper.(connectionMade|loseConnection|dataReceived) - - self.(loseConnection|write|writeSequence) - - AsyncStateMachine.(setCloseOp|setWriteOp) - """ - - #WARNING: IF YOU COPY-AND-PASTE THE ABOVE CODE, BE SURE TO REMOVE - #THE EXTRA ESCAPING AROUND "\\r\\n" - - def __init__(self, factory, wrappedProtocol): - ProtocolWrapper.__init__(self, factory, wrappedProtocol) - AsyncStateMachine.__init__(self) - self.fakeSocket = _FakeSocket(self) - self.tlsConnection = TLSConnection(self.fakeSocket) - self.tlsStarted = False - self.connectionLostCalled = False - - def connectionMade(self): - try: - ProtocolWrapper.connectionMade(self) - except TLSError, e: - self.connectionLost(Failure(e)) - ProtocolWrapper.loseConnection(self) - - def dataReceived(self, data): - try: - if not self.tlsStarted: - ProtocolWrapper.dataReceived(self, data) - else: - self.fakeSocket.data += data - while self.fakeSocket.data: - AsyncStateMachine.inReadEvent(self) - except TLSError, e: - self.connectionLost(Failure(e)) - ProtocolWrapper.loseConnection(self) - - def connectionLost(self, reason): - if not self.connectionLostCalled: - ProtocolWrapper.connectionLost(self, reason) - self.connectionLostCalled = True - - - def outConnectEvent(self): - ProtocolWrapper.connectionMade(self) - - def outCloseEvent(self): - ProtocolWrapper.loseConnection(self) - - def outReadEvent(self, data): - if data == "": - ProtocolWrapper.loseConnection(self) - else: - ProtocolWrapper.dataReceived(self, data) - - - def setServerHandshakeOp(self, **args): - self.tlsStarted = True - AsyncStateMachine.setServerHandshakeOp(self, **args) - - def loseConnection(self): - if not self.tlsStarted: - ProtocolWrapper.loseConnection(self) - else: - AsyncStateMachine.setCloseOp(self) - - def write(self, data): - if not self.tlsStarted: - ProtocolWrapper.write(self, data) - else: - #Because of the FakeSocket, write operations are guaranteed to - #terminate immediately. - AsyncStateMachine.setWriteOp(self, data) - - def writeSequence(self, seq): - if not self.tlsStarted: - ProtocolWrapper.writeSequence(self, seq) - else: - #Because of the FakeSocket, write operations are guaranteed to - #terminate immediately. - AsyncStateMachine.setWriteOp(self, "".join(seq)) +"""TLS Lite + Twisted.""" + +from twisted.protocols.policies import ProtocolWrapper, WrappingFactory +from twisted.python.failure import Failure + +from AsyncStateMachine import AsyncStateMachine +from gdata.tlslite.TLSConnection import TLSConnection +from gdata.tlslite.errors import * + +import socket +import errno + + +#The TLSConnection is created around a "fake socket" that +#plugs it into the underlying Twisted transport +class _FakeSocket: + def __init__(self, wrapper): + self.wrapper = wrapper + self.data = "" + + def send(self, data): + ProtocolWrapper.write(self.wrapper, data) + return len(data) + + def recv(self, numBytes): + if self.data == "": + raise socket.error, (errno.EWOULDBLOCK, "") + returnData = self.data[:numBytes] + self.data = self.data[numBytes:] + return returnData + +class TLSTwistedProtocolWrapper(ProtocolWrapper, AsyncStateMachine): + """This class can wrap Twisted protocols to add TLS support. + + Below is a complete example of using TLS Lite with a Twisted echo + server. + + There are two server implementations below. Echo is the original + protocol, which is oblivious to TLS. Echo1 subclasses Echo and + negotiates TLS when the client connects. Echo2 subclasses Echo and + negotiates TLS when the client sends "STARTTLS":: + + from twisted.internet.protocol import Protocol, Factory + from twisted.internet import reactor + from twisted.protocols.policies import WrappingFactory + from twisted.protocols.basic import LineReceiver + from twisted.python import log + from twisted.python.failure import Failure + import sys + from tlslite.api import * + + s = open("./serverX509Cert.pem").read() + x509 = X509() + x509.parse(s) + certChain = X509CertChain([x509]) + + s = open("./serverX509Key.pem").read() + privateKey = parsePEMKey(s, private=True) + + verifierDB = VerifierDB("verifierDB") + verifierDB.open() + + class Echo(LineReceiver): + def connectionMade(self): + self.transport.write("Welcome to the echo server!\\r\\n") + + def lineReceived(self, line): + self.transport.write(line + "\\r\\n") + + class Echo1(Echo): + def connectionMade(self): + if not self.transport.tlsStarted: + self.transport.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey, + verifierDB=verifierDB) + else: + Echo.connectionMade(self) + + def connectionLost(self, reason): + pass #Handle any TLS exceptions here + + class Echo2(Echo): + def lineReceived(self, data): + if data == "STARTTLS": + self.transport.setServerHandshakeOp(certChain=certChain, + privateKey=privateKey, + verifierDB=verifierDB) + else: + Echo.lineReceived(self, data) + + def connectionLost(self, reason): + pass #Handle any TLS exceptions here + + factory = Factory() + factory.protocol = Echo1 + #factory.protocol = Echo2 + + wrappingFactory = WrappingFactory(factory) + wrappingFactory.protocol = TLSTwistedProtocolWrapper + + log.startLogging(sys.stdout) + reactor.listenTCP(1079, wrappingFactory) + reactor.run() + + This class works as follows: + + Data comes in and is given to the AsyncStateMachine for handling. + AsyncStateMachine will forward events to this class, and we'll + pass them on to the ProtocolHandler, which will proxy them to the + wrapped protocol. The wrapped protocol may then call back into + this class, and these calls will be proxied into the + AsyncStateMachine. + + The call graph looks like this: + - self.dataReceived + - AsyncStateMachine.inReadEvent + - self.out(Connect|Close|Read)Event + - ProtocolWrapper.(connectionMade|loseConnection|dataReceived) + - self.(loseConnection|write|writeSequence) + - AsyncStateMachine.(setCloseOp|setWriteOp) + """ + + #WARNING: IF YOU COPY-AND-PASTE THE ABOVE CODE, BE SURE TO REMOVE + #THE EXTRA ESCAPING AROUND "\\r\\n" + + def __init__(self, factory, wrappedProtocol): + ProtocolWrapper.__init__(self, factory, wrappedProtocol) + AsyncStateMachine.__init__(self) + self.fakeSocket = _FakeSocket(self) + self.tlsConnection = TLSConnection(self.fakeSocket) + self.tlsStarted = False + self.connectionLostCalled = False + + def connectionMade(self): + try: + ProtocolWrapper.connectionMade(self) + except TLSError, e: + self.connectionLost(Failure(e)) + ProtocolWrapper.loseConnection(self) + + def dataReceived(self, data): + try: + if not self.tlsStarted: + ProtocolWrapper.dataReceived(self, data) + else: + self.fakeSocket.data += data + while self.fakeSocket.data: + AsyncStateMachine.inReadEvent(self) + except TLSError, e: + self.connectionLost(Failure(e)) + ProtocolWrapper.loseConnection(self) + + def connectionLost(self, reason): + if not self.connectionLostCalled: + ProtocolWrapper.connectionLost(self, reason) + self.connectionLostCalled = True + + + def outConnectEvent(self): + ProtocolWrapper.connectionMade(self) + + def outCloseEvent(self): + ProtocolWrapper.loseConnection(self) + + def outReadEvent(self, data): + if data == "": + ProtocolWrapper.loseConnection(self) + else: + ProtocolWrapper.dataReceived(self, data) + + + def setServerHandshakeOp(self, **args): + self.tlsStarted = True + AsyncStateMachine.setServerHandshakeOp(self, **args) + + def loseConnection(self): + if not self.tlsStarted: + ProtocolWrapper.loseConnection(self) + else: + AsyncStateMachine.setCloseOp(self) + + def write(self, data): + if not self.tlsStarted: + ProtocolWrapper.write(self, data) + else: + #Because of the FakeSocket, write operations are guaranteed to + #terminate immediately. + AsyncStateMachine.setWriteOp(self, data) + + def writeSequence(self, seq): + if not self.tlsStarted: + ProtocolWrapper.writeSequence(self, seq) + else: + #Because of the FakeSocket, write operations are guaranteed to + #terminate immediately. + AsyncStateMachine.setWriteOp(self, "".join(seq)) diff --git a/gdata/tlslite/integration/XMLRPCTransport.py b/src/gdata/tlslite/integration/XMLRPCTransport.py similarity index 100% rename from gdata/tlslite/integration/XMLRPCTransport.py rename to src/gdata/tlslite/integration/XMLRPCTransport.py diff --git a/gdata/tlslite/integration/__init__.py b/src/gdata/tlslite/integration/__init__.py similarity index 100% rename from gdata/tlslite/integration/__init__.py rename to src/gdata/tlslite/integration/__init__.py diff --git a/gdata/tlslite/mathtls.py b/src/gdata/tlslite/mathtls.py similarity index 100% rename from gdata/tlslite/mathtls.py rename to src/gdata/tlslite/mathtls.py diff --git a/gdata/tlslite/messages.py b/src/gdata/tlslite/messages.py similarity index 100% rename from gdata/tlslite/messages.py rename to src/gdata/tlslite/messages.py diff --git a/gdata/tlslite/utils/AES.py b/src/gdata/tlslite/utils/AES.py similarity index 100% rename from gdata/tlslite/utils/AES.py rename to src/gdata/tlslite/utils/AES.py diff --git a/gdata/tlslite/utils/ASN1Parser.py b/src/gdata/tlslite/utils/ASN1Parser.py similarity index 100% rename from gdata/tlslite/utils/ASN1Parser.py rename to src/gdata/tlslite/utils/ASN1Parser.py diff --git a/gdata/tlslite/utils/Cryptlib_AES.py b/src/gdata/tlslite/utils/Cryptlib_AES.py similarity index 100% rename from gdata/tlslite/utils/Cryptlib_AES.py rename to src/gdata/tlslite/utils/Cryptlib_AES.py diff --git a/gdata/tlslite/utils/Cryptlib_RC4.py b/src/gdata/tlslite/utils/Cryptlib_RC4.py similarity index 100% rename from gdata/tlslite/utils/Cryptlib_RC4.py rename to src/gdata/tlslite/utils/Cryptlib_RC4.py diff --git a/gdata/tlslite/utils/Cryptlib_TripleDES.py b/src/gdata/tlslite/utils/Cryptlib_TripleDES.py similarity index 100% rename from gdata/tlslite/utils/Cryptlib_TripleDES.py rename to src/gdata/tlslite/utils/Cryptlib_TripleDES.py diff --git a/gdata/tlslite/utils/OpenSSL_AES.py b/src/gdata/tlslite/utils/OpenSSL_AES.py similarity index 100% rename from gdata/tlslite/utils/OpenSSL_AES.py rename to src/gdata/tlslite/utils/OpenSSL_AES.py diff --git a/gdata/tlslite/utils/OpenSSL_RC4.py b/src/gdata/tlslite/utils/OpenSSL_RC4.py similarity index 100% rename from gdata/tlslite/utils/OpenSSL_RC4.py rename to src/gdata/tlslite/utils/OpenSSL_RC4.py diff --git a/gdata/tlslite/utils/OpenSSL_RSAKey.py b/src/gdata/tlslite/utils/OpenSSL_RSAKey.py similarity index 100% rename from gdata/tlslite/utils/OpenSSL_RSAKey.py rename to src/gdata/tlslite/utils/OpenSSL_RSAKey.py diff --git a/gdata/tlslite/utils/OpenSSL_TripleDES.py b/src/gdata/tlslite/utils/OpenSSL_TripleDES.py similarity index 100% rename from gdata/tlslite/utils/OpenSSL_TripleDES.py rename to src/gdata/tlslite/utils/OpenSSL_TripleDES.py diff --git a/gdata/tlslite/utils/PyCrypto_AES.py b/src/gdata/tlslite/utils/PyCrypto_AES.py similarity index 100% rename from gdata/tlslite/utils/PyCrypto_AES.py rename to src/gdata/tlslite/utils/PyCrypto_AES.py diff --git a/gdata/tlslite/utils/PyCrypto_RC4.py b/src/gdata/tlslite/utils/PyCrypto_RC4.py similarity index 100% rename from gdata/tlslite/utils/PyCrypto_RC4.py rename to src/gdata/tlslite/utils/PyCrypto_RC4.py diff --git a/gdata/tlslite/utils/PyCrypto_RSAKey.py b/src/gdata/tlslite/utils/PyCrypto_RSAKey.py similarity index 100% rename from gdata/tlslite/utils/PyCrypto_RSAKey.py rename to src/gdata/tlslite/utils/PyCrypto_RSAKey.py diff --git a/gdata/tlslite/utils/PyCrypto_TripleDES.py b/src/gdata/tlslite/utils/PyCrypto_TripleDES.py similarity index 100% rename from gdata/tlslite/utils/PyCrypto_TripleDES.py rename to src/gdata/tlslite/utils/PyCrypto_TripleDES.py diff --git a/gdata/tlslite/utils/Python_AES.py b/src/gdata/tlslite/utils/Python_AES.py similarity index 100% rename from gdata/tlslite/utils/Python_AES.py rename to src/gdata/tlslite/utils/Python_AES.py diff --git a/gdata/tlslite/utils/Python_RC4.py b/src/gdata/tlslite/utils/Python_RC4.py similarity index 100% rename from gdata/tlslite/utils/Python_RC4.py rename to src/gdata/tlslite/utils/Python_RC4.py diff --git a/gdata/tlslite/utils/Python_RSAKey.py b/src/gdata/tlslite/utils/Python_RSAKey.py similarity index 99% rename from gdata/tlslite/utils/Python_RSAKey.py rename to src/gdata/tlslite/utils/Python_RSAKey.py index 2c469b57..b57553a1 100644 --- a/gdata/tlslite/utils/Python_RSAKey.py +++ b/src/gdata/tlslite/utils/Python_RSAKey.py @@ -1,5 +1,5 @@ -"""Pure-Python RSA implementation.""" - +"""Pure-Python RSA implementation.""" + from cryptomath import * import xmltools from ASN1Parser import ASN1Parser @@ -8,7 +8,7 @@ from RSAKey import * class Python_RSAKey(RSAKey): def __init__(self, n=0, e=0, d=0, p=0, q=0, dP=0, dQ=0, qInv=0): if (n and not e) or (e and not n): - raise AssertionError() + raise AssertionError() self.n = n self.e = e self.d = d @@ -122,61 +122,61 @@ class Python_RSAKey(RSAKey): bytes = base64ToBytes(s) return Python_RSAKey._parsePKCS8(bytes) else: - start = s.find("-----BEGIN RSA PRIVATE KEY-----") - if start != -1: - end = s.find("-----END RSA PRIVATE KEY-----") - if end == -1: - raise SyntaxError("Missing PEM Postfix") - s = s[start+len("-----BEGIN RSA PRIVATE KEY -----") : end] - bytes = base64ToBytes(s) - return Python_RSAKey._parseSSLeay(bytes) - raise SyntaxError("Missing PEM Prefix") - parsePEM = staticmethod(parsePEM) - - def parseXML(s): + start = s.find("-----BEGIN RSA PRIVATE KEY-----") + if start != -1: + end = s.find("-----END RSA PRIVATE KEY-----") + if end == -1: + raise SyntaxError("Missing PEM Postfix") + s = s[start+len("-----BEGIN RSA PRIVATE KEY -----") : end] + bytes = base64ToBytes(s) + return Python_RSAKey._parseSSLeay(bytes) + raise SyntaxError("Missing PEM Prefix") + parsePEM = staticmethod(parsePEM) + + def parseXML(s): element = xmltools.parseAndStripWhitespace(s) - return Python_RSAKey._parseXML(element) + return Python_RSAKey._parseXML(element) parseXML = staticmethod(parseXML) - - def _parsePKCS8(bytes): - p = ASN1Parser(bytes) - - version = p.getChild(0).value[0] - if version != 0: - raise SyntaxError("Unrecognized PKCS8 version") - - rsaOID = p.getChild(1).value - if list(rsaOID) != [6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]: - raise SyntaxError("Unrecognized AlgorithmIdentifier") - - #Get the privateKey - privateKeyP = p.getChild(2) - - #Adjust for OCTET STRING encapsulation - privateKeyP = ASN1Parser(privateKeyP.value) - - return Python_RSAKey._parseASN1PrivateKey(privateKeyP) - _parsePKCS8 = staticmethod(_parsePKCS8) - - def _parseSSLeay(bytes): - privateKeyP = ASN1Parser(bytes) - return Python_RSAKey._parseASN1PrivateKey(privateKeyP) + + def _parsePKCS8(bytes): + p = ASN1Parser(bytes) + + version = p.getChild(0).value[0] + if version != 0: + raise SyntaxError("Unrecognized PKCS8 version") + + rsaOID = p.getChild(1).value + if list(rsaOID) != [6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 1, 5, 0]: + raise SyntaxError("Unrecognized AlgorithmIdentifier") + + #Get the privateKey + privateKeyP = p.getChild(2) + + #Adjust for OCTET STRING encapsulation + privateKeyP = ASN1Parser(privateKeyP.value) + + return Python_RSAKey._parseASN1PrivateKey(privateKeyP) + _parsePKCS8 = staticmethod(_parsePKCS8) + + def _parseSSLeay(bytes): + privateKeyP = ASN1Parser(bytes) + return Python_RSAKey._parseASN1PrivateKey(privateKeyP) _parseSSLeay = staticmethod(_parseSSLeay) - - def _parseASN1PrivateKey(privateKeyP): - version = privateKeyP.getChild(0).value[0] - if version != 0: - raise SyntaxError("Unrecognized RSAPrivateKey version") - n = bytesToNumber(privateKeyP.getChild(1).value) - e = bytesToNumber(privateKeyP.getChild(2).value) - d = bytesToNumber(privateKeyP.getChild(3).value) - p = bytesToNumber(privateKeyP.getChild(4).value) - q = bytesToNumber(privateKeyP.getChild(5).value) - dP = bytesToNumber(privateKeyP.getChild(6).value) - dQ = bytesToNumber(privateKeyP.getChild(7).value) - qInv = bytesToNumber(privateKeyP.getChild(8).value) - return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) - _parseASN1PrivateKey = staticmethod(_parseASN1PrivateKey) + + def _parseASN1PrivateKey(privateKeyP): + version = privateKeyP.getChild(0).value[0] + if version != 0: + raise SyntaxError("Unrecognized RSAPrivateKey version") + n = bytesToNumber(privateKeyP.getChild(1).value) + e = bytesToNumber(privateKeyP.getChild(2).value) + d = bytesToNumber(privateKeyP.getChild(3).value) + p = bytesToNumber(privateKeyP.getChild(4).value) + q = bytesToNumber(privateKeyP.getChild(5).value) + dP = bytesToNumber(privateKeyP.getChild(6).value) + dQ = bytesToNumber(privateKeyP.getChild(7).value) + qInv = bytesToNumber(privateKeyP.getChild(8).value) + return Python_RSAKey(n, e, d, p, q, dP, dQ, qInv) + _parseASN1PrivateKey = staticmethod(_parseASN1PrivateKey) def _parseXML(element): try: diff --git a/gdata/tlslite/utils/RC4.py b/src/gdata/tlslite/utils/RC4.py similarity index 100% rename from gdata/tlslite/utils/RC4.py rename to src/gdata/tlslite/utils/RC4.py diff --git a/gdata/tlslite/utils/RSAKey.py b/src/gdata/tlslite/utils/RSAKey.py similarity index 100% rename from gdata/tlslite/utils/RSAKey.py rename to src/gdata/tlslite/utils/RSAKey.py diff --git a/gdata/tlslite/utils/TripleDES.py b/src/gdata/tlslite/utils/TripleDES.py similarity index 100% rename from gdata/tlslite/utils/TripleDES.py rename to src/gdata/tlslite/utils/TripleDES.py diff --git a/gdata/tlslite/utils/__init__.py b/src/gdata/tlslite/utils/__init__.py similarity index 100% rename from gdata/tlslite/utils/__init__.py rename to src/gdata/tlslite/utils/__init__.py diff --git a/gdata/tlslite/utils/cipherfactory.py b/src/gdata/tlslite/utils/cipherfactory.py similarity index 100% rename from gdata/tlslite/utils/cipherfactory.py rename to src/gdata/tlslite/utils/cipherfactory.py diff --git a/gdata/tlslite/utils/codec.py b/src/gdata/tlslite/utils/codec.py similarity index 100% rename from gdata/tlslite/utils/codec.py rename to src/gdata/tlslite/utils/codec.py diff --git a/gdata/tlslite/utils/compat.py b/src/gdata/tlslite/utils/compat.py similarity index 100% rename from gdata/tlslite/utils/compat.py rename to src/gdata/tlslite/utils/compat.py diff --git a/gdata/tlslite/utils/cryptomath.py b/src/gdata/tlslite/utils/cryptomath.py similarity index 100% rename from gdata/tlslite/utils/cryptomath.py rename to src/gdata/tlslite/utils/cryptomath.py diff --git a/gdata/tlslite/utils/dateFuncs.py b/src/gdata/tlslite/utils/dateFuncs.py similarity index 100% rename from gdata/tlslite/utils/dateFuncs.py rename to src/gdata/tlslite/utils/dateFuncs.py diff --git a/gdata/tlslite/utils/entropy.c b/src/gdata/tlslite/utils/entropy.c similarity index 100% rename from gdata/tlslite/utils/entropy.c rename to src/gdata/tlslite/utils/entropy.c diff --git a/gdata/tlslite/utils/hmac.py b/src/gdata/tlslite/utils/hmac.py similarity index 100% rename from gdata/tlslite/utils/hmac.py rename to src/gdata/tlslite/utils/hmac.py diff --git a/gdata/tlslite/utils/jython_compat.py b/src/gdata/tlslite/utils/jython_compat.py similarity index 100% rename from gdata/tlslite/utils/jython_compat.py rename to src/gdata/tlslite/utils/jython_compat.py diff --git a/gdata/tlslite/utils/keyfactory.py b/src/gdata/tlslite/utils/keyfactory.py similarity index 100% rename from gdata/tlslite/utils/keyfactory.py rename to src/gdata/tlslite/utils/keyfactory.py diff --git a/gdata/tlslite/utils/rijndael.py b/src/gdata/tlslite/utils/rijndael.py similarity index 100% rename from gdata/tlslite/utils/rijndael.py rename to src/gdata/tlslite/utils/rijndael.py diff --git a/gdata/tlslite/utils/win32prng.c b/src/gdata/tlslite/utils/win32prng.c similarity index 100% rename from gdata/tlslite/utils/win32prng.c rename to src/gdata/tlslite/utils/win32prng.c diff --git a/gdata/tlslite/utils/xmltools.py b/src/gdata/tlslite/utils/xmltools.py similarity index 100% rename from gdata/tlslite/utils/xmltools.py rename to src/gdata/tlslite/utils/xmltools.py diff --git a/gdata/urlfetch.py b/src/gdata/urlfetch.py similarity index 100% rename from gdata/urlfetch.py rename to src/gdata/urlfetch.py diff --git a/googleapiclient/__init__.py b/src/googleapiclient/__init__.py similarity index 100% rename from googleapiclient/__init__.py rename to src/googleapiclient/__init__.py diff --git a/googleapiclient/channel.py b/src/googleapiclient/channel.py similarity index 100% rename from googleapiclient/channel.py rename to src/googleapiclient/channel.py diff --git a/googleapiclient/discovery.py b/src/googleapiclient/discovery.py similarity index 100% rename from googleapiclient/discovery.py rename to src/googleapiclient/discovery.py diff --git a/googleapiclient/discovery_cache/__init__.py b/src/googleapiclient/discovery_cache/__init__.py similarity index 100% rename from googleapiclient/discovery_cache/__init__.py rename to src/googleapiclient/discovery_cache/__init__.py diff --git a/googleapiclient/discovery_cache/appengine_memcache.py b/src/googleapiclient/discovery_cache/appengine_memcache.py similarity index 100% rename from googleapiclient/discovery_cache/appengine_memcache.py rename to src/googleapiclient/discovery_cache/appengine_memcache.py diff --git a/googleapiclient/discovery_cache/base.py b/src/googleapiclient/discovery_cache/base.py similarity index 100% rename from googleapiclient/discovery_cache/base.py rename to src/googleapiclient/discovery_cache/base.py diff --git a/googleapiclient/discovery_cache/file_cache.py b/src/googleapiclient/discovery_cache/file_cache.py similarity index 100% rename from googleapiclient/discovery_cache/file_cache.py rename to src/googleapiclient/discovery_cache/file_cache.py diff --git a/googleapiclient/errors.py b/src/googleapiclient/errors.py similarity index 100% rename from googleapiclient/errors.py rename to src/googleapiclient/errors.py diff --git a/googleapiclient/http.py b/src/googleapiclient/http.py similarity index 100% rename from googleapiclient/http.py rename to src/googleapiclient/http.py diff --git a/googleapiclient/mimeparse.py b/src/googleapiclient/mimeparse.py similarity index 100% rename from googleapiclient/mimeparse.py rename to src/googleapiclient/mimeparse.py diff --git a/googleapiclient/model.py b/src/googleapiclient/model.py similarity index 100% rename from googleapiclient/model.py rename to src/googleapiclient/model.py diff --git a/googleapiclient/sample_tools.py b/src/googleapiclient/sample_tools.py similarity index 100% rename from googleapiclient/sample_tools.py rename to src/googleapiclient/sample_tools.py diff --git a/googleapiclient/schema.py b/src/googleapiclient/schema.py similarity index 100% rename from googleapiclient/schema.py rename to src/googleapiclient/schema.py diff --git a/httplib2/__init__.py b/src/httplib2/__init__.py similarity index 100% rename from httplib2/__init__.py rename to src/httplib2/__init__.py diff --git a/httplib2/cacerts.txt b/src/httplib2/cacerts.txt similarity index 100% rename from httplib2/cacerts.txt rename to src/httplib2/cacerts.txt diff --git a/httplib2/iri2uri.py b/src/httplib2/iri2uri.py similarity index 100% rename from httplib2/iri2uri.py rename to src/httplib2/iri2uri.py diff --git a/httplib2/socks.py b/src/httplib2/socks.py similarity index 100% rename from httplib2/socks.py rename to src/httplib2/socks.py diff --git a/httplib2/test/__init__.py b/src/httplib2/test/__init__.py similarity index 100% rename from httplib2/test/__init__.py rename to src/httplib2/test/__init__.py diff --git a/httplib2/test/brokensocket/socket.py b/src/httplib2/test/brokensocket/socket.py similarity index 100% rename from httplib2/test/brokensocket/socket.py rename to src/httplib2/test/brokensocket/socket.py diff --git a/httplib2/test/functional/test_proxies.py b/src/httplib2/test/functional/test_proxies.py similarity index 100% rename from httplib2/test/functional/test_proxies.py rename to src/httplib2/test/functional/test_proxies.py diff --git a/httplib2/test/miniserver.py b/src/httplib2/test/miniserver.py similarity index 100% rename from httplib2/test/miniserver.py rename to src/httplib2/test/miniserver.py diff --git a/httplib2/test/other_cacerts.txt b/src/httplib2/test/other_cacerts.txt similarity index 100% rename from httplib2/test/other_cacerts.txt rename to src/httplib2/test/other_cacerts.txt diff --git a/httplib2/test/smoke_test.py b/src/httplib2/test/smoke_test.py similarity index 100% rename from httplib2/test/smoke_test.py rename to src/httplib2/test/smoke_test.py diff --git a/httplib2/test/test_no_socket.py b/src/httplib2/test/test_no_socket.py similarity index 100% rename from httplib2/test/test_no_socket.py rename to src/httplib2/test/test_no_socket.py diff --git a/oauth2client/__init__.py b/src/oauth2client/__init__.py similarity index 100% rename from oauth2client/__init__.py rename to src/oauth2client/__init__.py diff --git a/oauth2client/_helpers.py b/src/oauth2client/_helpers.py similarity index 100% rename from oauth2client/_helpers.py rename to src/oauth2client/_helpers.py diff --git a/oauth2client/_openssl_crypt.py b/src/oauth2client/_openssl_crypt.py similarity index 100% rename from oauth2client/_openssl_crypt.py rename to src/oauth2client/_openssl_crypt.py diff --git a/oauth2client/_pycrypto_crypt.py b/src/oauth2client/_pycrypto_crypt.py similarity index 100% rename from oauth2client/_pycrypto_crypt.py rename to src/oauth2client/_pycrypto_crypt.py diff --git a/oauth2client/appengine.py b/src/oauth2client/appengine.py similarity index 100% rename from oauth2client/appengine.py rename to src/oauth2client/appengine.py diff --git a/oauth2client/client.py b/src/oauth2client/client.py similarity index 100% rename from oauth2client/client.py rename to src/oauth2client/client.py diff --git a/oauth2client/clientsecrets.py b/src/oauth2client/clientsecrets.py similarity index 100% rename from oauth2client/clientsecrets.py rename to src/oauth2client/clientsecrets.py diff --git a/oauth2client/crypt.py b/src/oauth2client/crypt.py similarity index 100% rename from oauth2client/crypt.py rename to src/oauth2client/crypt.py diff --git a/oauth2client/devshell.py b/src/oauth2client/devshell.py similarity index 100% rename from oauth2client/devshell.py rename to src/oauth2client/devshell.py diff --git a/oauth2client/django_orm.py b/src/oauth2client/django_orm.py similarity index 100% rename from oauth2client/django_orm.py rename to src/oauth2client/django_orm.py diff --git a/oauth2client/file.py b/src/oauth2client/file.py similarity index 100% rename from oauth2client/file.py rename to src/oauth2client/file.py diff --git a/oauth2client/flask_util.py b/src/oauth2client/flask_util.py similarity index 100% rename from oauth2client/flask_util.py rename to src/oauth2client/flask_util.py diff --git a/oauth2client/gce.py b/src/oauth2client/gce.py similarity index 100% rename from oauth2client/gce.py rename to src/oauth2client/gce.py diff --git a/oauth2client/keyring_storage.py b/src/oauth2client/keyring_storage.py similarity index 100% rename from oauth2client/keyring_storage.py rename to src/oauth2client/keyring_storage.py diff --git a/oauth2client/locked_file.py b/src/oauth2client/locked_file.py similarity index 100% rename from oauth2client/locked_file.py rename to src/oauth2client/locked_file.py diff --git a/oauth2client/multistore_file.py b/src/oauth2client/multistore_file.py similarity index 100% rename from oauth2client/multistore_file.py rename to src/oauth2client/multistore_file.py diff --git a/oauth2client/old_run.py b/src/oauth2client/old_run.py similarity index 100% rename from oauth2client/old_run.py rename to src/oauth2client/old_run.py diff --git a/oauth2client/service_account.py b/src/oauth2client/service_account.py similarity index 100% rename from oauth2client/service_account.py rename to src/oauth2client/service_account.py diff --git a/oauth2client/tools.py b/src/oauth2client/tools.py similarity index 100% rename from oauth2client/tools.py rename to src/oauth2client/tools.py diff --git a/oauth2client/util.py b/src/oauth2client/util.py similarity index 100% rename from oauth2client/util.py rename to src/oauth2client/util.py diff --git a/oauth2client/xsrfutil.py b/src/oauth2client/xsrfutil.py similarity index 100% rename from oauth2client/xsrfutil.py rename to src/oauth2client/xsrfutil.py diff --git a/passlib/__init__.py b/src/passlib/__init__.py similarity index 100% rename from passlib/__init__.py rename to src/passlib/__init__.py diff --git a/passlib/_setup/__init__.py b/src/passlib/_setup/__init__.py similarity index 100% rename from passlib/_setup/__init__.py rename to src/passlib/_setup/__init__.py diff --git a/passlib/_setup/docdist.py b/src/passlib/_setup/docdist.py similarity index 100% rename from passlib/_setup/docdist.py rename to src/passlib/_setup/docdist.py diff --git a/passlib/_setup/stamp.py b/src/passlib/_setup/stamp.py similarity index 100% rename from passlib/_setup/stamp.py rename to src/passlib/_setup/stamp.py diff --git a/passlib/apache.py b/src/passlib/apache.py similarity index 100% rename from passlib/apache.py rename to src/passlib/apache.py diff --git a/passlib/apps.py b/src/passlib/apps.py similarity index 100% rename from passlib/apps.py rename to src/passlib/apps.py diff --git a/passlib/context.py b/src/passlib/context.py similarity index 100% rename from passlib/context.py rename to src/passlib/context.py diff --git a/passlib/exc.py b/src/passlib/exc.py similarity index 100% rename from passlib/exc.py rename to src/passlib/exc.py diff --git a/passlib/ext/__init__.py b/src/passlib/ext/__init__.py similarity index 100% rename from passlib/ext/__init__.py rename to src/passlib/ext/__init__.py diff --git a/passlib/ext/django/__init__.py b/src/passlib/ext/django/__init__.py similarity index 100% rename from passlib/ext/django/__init__.py rename to src/passlib/ext/django/__init__.py diff --git a/passlib/ext/django/models.py b/src/passlib/ext/django/models.py similarity index 100% rename from passlib/ext/django/models.py rename to src/passlib/ext/django/models.py diff --git a/passlib/ext/django/utils.py b/src/passlib/ext/django/utils.py similarity index 100% rename from passlib/ext/django/utils.py rename to src/passlib/ext/django/utils.py diff --git a/passlib/handlers/__init__.py b/src/passlib/handlers/__init__.py similarity index 100% rename from passlib/handlers/__init__.py rename to src/passlib/handlers/__init__.py diff --git a/passlib/handlers/bcrypt.py b/src/passlib/handlers/bcrypt.py similarity index 100% rename from passlib/handlers/bcrypt.py rename to src/passlib/handlers/bcrypt.py diff --git a/passlib/handlers/cisco.py b/src/passlib/handlers/cisco.py similarity index 100% rename from passlib/handlers/cisco.py rename to src/passlib/handlers/cisco.py diff --git a/passlib/handlers/des_crypt.py b/src/passlib/handlers/des_crypt.py similarity index 100% rename from passlib/handlers/des_crypt.py rename to src/passlib/handlers/des_crypt.py diff --git a/passlib/handlers/digests.py b/src/passlib/handlers/digests.py similarity index 100% rename from passlib/handlers/digests.py rename to src/passlib/handlers/digests.py diff --git a/passlib/handlers/django.py b/src/passlib/handlers/django.py similarity index 100% rename from passlib/handlers/django.py rename to src/passlib/handlers/django.py diff --git a/passlib/handlers/fshp.py b/src/passlib/handlers/fshp.py similarity index 100% rename from passlib/handlers/fshp.py rename to src/passlib/handlers/fshp.py diff --git a/passlib/handlers/ldap_digests.py b/src/passlib/handlers/ldap_digests.py similarity index 100% rename from passlib/handlers/ldap_digests.py rename to src/passlib/handlers/ldap_digests.py diff --git a/passlib/handlers/md5_crypt.py b/src/passlib/handlers/md5_crypt.py similarity index 100% rename from passlib/handlers/md5_crypt.py rename to src/passlib/handlers/md5_crypt.py diff --git a/passlib/handlers/misc.py b/src/passlib/handlers/misc.py similarity index 100% rename from passlib/handlers/misc.py rename to src/passlib/handlers/misc.py diff --git a/passlib/handlers/mssql.py b/src/passlib/handlers/mssql.py similarity index 100% rename from passlib/handlers/mssql.py rename to src/passlib/handlers/mssql.py diff --git a/passlib/handlers/mysql.py b/src/passlib/handlers/mysql.py similarity index 100% rename from passlib/handlers/mysql.py rename to src/passlib/handlers/mysql.py diff --git a/passlib/handlers/oracle.py b/src/passlib/handlers/oracle.py similarity index 100% rename from passlib/handlers/oracle.py rename to src/passlib/handlers/oracle.py diff --git a/passlib/handlers/pbkdf2.py b/src/passlib/handlers/pbkdf2.py similarity index 100% rename from passlib/handlers/pbkdf2.py rename to src/passlib/handlers/pbkdf2.py diff --git a/passlib/handlers/phpass.py b/src/passlib/handlers/phpass.py similarity index 100% rename from passlib/handlers/phpass.py rename to src/passlib/handlers/phpass.py diff --git a/passlib/handlers/postgres.py b/src/passlib/handlers/postgres.py similarity index 100% rename from passlib/handlers/postgres.py rename to src/passlib/handlers/postgres.py diff --git a/passlib/handlers/roundup.py b/src/passlib/handlers/roundup.py similarity index 100% rename from passlib/handlers/roundup.py rename to src/passlib/handlers/roundup.py diff --git a/passlib/handlers/scram.py b/src/passlib/handlers/scram.py similarity index 100% rename from passlib/handlers/scram.py rename to src/passlib/handlers/scram.py diff --git a/passlib/handlers/sha1_crypt.py b/src/passlib/handlers/sha1_crypt.py similarity index 100% rename from passlib/handlers/sha1_crypt.py rename to src/passlib/handlers/sha1_crypt.py diff --git a/passlib/handlers/sha2_crypt.py b/src/passlib/handlers/sha2_crypt.py similarity index 100% rename from passlib/handlers/sha2_crypt.py rename to src/passlib/handlers/sha2_crypt.py diff --git a/passlib/handlers/sun_md5_crypt.py b/src/passlib/handlers/sun_md5_crypt.py similarity index 100% rename from passlib/handlers/sun_md5_crypt.py rename to src/passlib/handlers/sun_md5_crypt.py diff --git a/passlib/handlers/windows.py b/src/passlib/handlers/windows.py similarity index 100% rename from passlib/handlers/windows.py rename to src/passlib/handlers/windows.py diff --git a/passlib/hash.py b/src/passlib/hash.py similarity index 100% rename from passlib/hash.py rename to src/passlib/hash.py diff --git a/passlib/hosts.py b/src/passlib/hosts.py similarity index 100% rename from passlib/hosts.py rename to src/passlib/hosts.py diff --git a/passlib/ifc.py b/src/passlib/ifc.py similarity index 100% rename from passlib/ifc.py rename to src/passlib/ifc.py diff --git a/passlib/registry.py b/src/passlib/registry.py similarity index 100% rename from passlib/registry.py rename to src/passlib/registry.py diff --git a/passlib/tests/__init__.py b/src/passlib/tests/__init__.py similarity index 100% rename from passlib/tests/__init__.py rename to src/passlib/tests/__init__.py diff --git a/passlib/tests/__main__.py b/src/passlib/tests/__main__.py similarity index 100% rename from passlib/tests/__main__.py rename to src/passlib/tests/__main__.py diff --git a/passlib/tests/_test_bad_register.py b/src/passlib/tests/_test_bad_register.py similarity index 100% rename from passlib/tests/_test_bad_register.py rename to src/passlib/tests/_test_bad_register.py diff --git a/passlib/tests/backports.py b/src/passlib/tests/backports.py similarity index 100% rename from passlib/tests/backports.py rename to src/passlib/tests/backports.py diff --git a/passlib/tests/sample1.cfg b/src/passlib/tests/sample1.cfg similarity index 100% rename from passlib/tests/sample1.cfg rename to src/passlib/tests/sample1.cfg diff --git a/passlib/tests/sample1b.cfg b/src/passlib/tests/sample1b.cfg similarity index 100% rename from passlib/tests/sample1b.cfg rename to src/passlib/tests/sample1b.cfg diff --git a/passlib/tests/sample1c.cfg b/src/passlib/tests/sample1c.cfg similarity index 100% rename from passlib/tests/sample1c.cfg rename to src/passlib/tests/sample1c.cfg diff --git a/passlib/tests/sample_config_1s.cfg b/src/passlib/tests/sample_config_1s.cfg similarity index 100% rename from passlib/tests/sample_config_1s.cfg rename to src/passlib/tests/sample_config_1s.cfg diff --git a/passlib/tests/test_apache.py b/src/passlib/tests/test_apache.py similarity index 100% rename from passlib/tests/test_apache.py rename to src/passlib/tests/test_apache.py diff --git a/passlib/tests/test_apps.py b/src/passlib/tests/test_apps.py similarity index 100% rename from passlib/tests/test_apps.py rename to src/passlib/tests/test_apps.py diff --git a/passlib/tests/test_context.py b/src/passlib/tests/test_context.py similarity index 100% rename from passlib/tests/test_context.py rename to src/passlib/tests/test_context.py diff --git a/passlib/tests/test_context_deprecated.py b/src/passlib/tests/test_context_deprecated.py similarity index 100% rename from passlib/tests/test_context_deprecated.py rename to src/passlib/tests/test_context_deprecated.py diff --git a/passlib/tests/test_ext_django.py b/src/passlib/tests/test_ext_django.py similarity index 100% rename from passlib/tests/test_ext_django.py rename to src/passlib/tests/test_ext_django.py diff --git a/passlib/tests/test_handlers.py b/src/passlib/tests/test_handlers.py similarity index 100% rename from passlib/tests/test_handlers.py rename to src/passlib/tests/test_handlers.py diff --git a/passlib/tests/test_handlers_bcrypt.py b/src/passlib/tests/test_handlers_bcrypt.py similarity index 100% rename from passlib/tests/test_handlers_bcrypt.py rename to src/passlib/tests/test_handlers_bcrypt.py diff --git a/passlib/tests/test_handlers_django.py b/src/passlib/tests/test_handlers_django.py similarity index 100% rename from passlib/tests/test_handlers_django.py rename to src/passlib/tests/test_handlers_django.py diff --git a/passlib/tests/test_hosts.py b/src/passlib/tests/test_hosts.py similarity index 100% rename from passlib/tests/test_hosts.py rename to src/passlib/tests/test_hosts.py diff --git a/passlib/tests/test_registry.py b/src/passlib/tests/test_registry.py similarity index 100% rename from passlib/tests/test_registry.py rename to src/passlib/tests/test_registry.py diff --git a/passlib/tests/test_utils.py b/src/passlib/tests/test_utils.py similarity index 100% rename from passlib/tests/test_utils.py rename to src/passlib/tests/test_utils.py diff --git a/passlib/tests/test_utils_crypto.py b/src/passlib/tests/test_utils_crypto.py similarity index 100% rename from passlib/tests/test_utils_crypto.py rename to src/passlib/tests/test_utils_crypto.py diff --git a/passlib/tests/test_utils_handlers.py b/src/passlib/tests/test_utils_handlers.py similarity index 100% rename from passlib/tests/test_utils_handlers.py rename to src/passlib/tests/test_utils_handlers.py diff --git a/passlib/tests/test_win32.py b/src/passlib/tests/test_win32.py similarity index 100% rename from passlib/tests/test_win32.py rename to src/passlib/tests/test_win32.py diff --git a/passlib/tests/tox_support.py b/src/passlib/tests/tox_support.py similarity index 100% rename from passlib/tests/tox_support.py rename to src/passlib/tests/tox_support.py diff --git a/passlib/tests/utils.py b/src/passlib/tests/utils.py similarity index 100% rename from passlib/tests/utils.py rename to src/passlib/tests/utils.py diff --git a/passlib/utils/__init__.py b/src/passlib/utils/__init__.py similarity index 100% rename from passlib/utils/__init__.py rename to src/passlib/utils/__init__.py diff --git a/passlib/utils/_blowfish/__init__.py b/src/passlib/utils/_blowfish/__init__.py similarity index 100% rename from passlib/utils/_blowfish/__init__.py rename to src/passlib/utils/_blowfish/__init__.py diff --git a/passlib/utils/_blowfish/_gen_files.py b/src/passlib/utils/_blowfish/_gen_files.py similarity index 100% rename from passlib/utils/_blowfish/_gen_files.py rename to src/passlib/utils/_blowfish/_gen_files.py diff --git a/passlib/utils/_blowfish/base.py b/src/passlib/utils/_blowfish/base.py similarity index 100% rename from passlib/utils/_blowfish/base.py rename to src/passlib/utils/_blowfish/base.py diff --git a/passlib/utils/_blowfish/unrolled.py b/src/passlib/utils/_blowfish/unrolled.py similarity index 100% rename from passlib/utils/_blowfish/unrolled.py rename to src/passlib/utils/_blowfish/unrolled.py diff --git a/passlib/utils/compat.py b/src/passlib/utils/compat.py similarity index 100% rename from passlib/utils/compat.py rename to src/passlib/utils/compat.py diff --git a/passlib/utils/des.py b/src/passlib/utils/des.py similarity index 100% rename from passlib/utils/des.py rename to src/passlib/utils/des.py diff --git a/passlib/utils/handlers.py b/src/passlib/utils/handlers.py similarity index 100% rename from passlib/utils/handlers.py rename to src/passlib/utils/handlers.py diff --git a/passlib/utils/md4.py b/src/passlib/utils/md4.py similarity index 100% rename from passlib/utils/md4.py rename to src/passlib/utils/md4.py diff --git a/passlib/utils/pbkdf2.py b/src/passlib/utils/pbkdf2.py similarity index 100% rename from passlib/utils/pbkdf2.py rename to src/passlib/utils/pbkdf2.py diff --git a/passlib/win32.py b/src/passlib/win32.py similarity index 100% rename from passlib/win32.py rename to src/passlib/win32.py diff --git a/six.py b/src/six.py similarity index 100% rename from six.py rename to src/six.py diff --git a/uritemplate/__init__.py b/src/uritemplate/__init__.py similarity index 100% rename from uritemplate/__init__.py rename to src/uritemplate/__init__.py diff --git a/whatsnew.txt b/src/whatsnew.txt similarity index 98% rename from whatsnew.txt rename to src/whatsnew.txt index 78cd0d0f..0c7aa061 100644 --- a/whatsnew.txt +++ b/src/whatsnew.txt @@ -1,316 +1,316 @@ -GAM 3.61 - -Various fixes by Ross Scroggs for Domain API and Data Transfer commands - -Remove duplicate DoCreateDomain which broke "gam create domain" - -GAM 3.6 - -Change your primary domain! - -Transfer Google Drive and Google+ data between users. - -Domains API support includes ability to add, delete, update and print domains. - -GAM 3.51 - -delete or trash messages in mailboxes based on a Gmail search. - -create and delete course aliases with Classroom API. - -HUGE number of bug fixes, large and small by Ross Scroggs. Many thanks Ross! - -GAM 3.5 - -Support for the new Google Classroom API. - -create, update, info and delete courses - -add, remove and sync course teachers and students - -print courses and course participants - -Google CloudPrint API Support - -update, info, delete and report printers - -share, unshare and get ACLs for printers - -submit, cancel, report and delete print jobs - -Bug fixes and improvements to GAM batch commands - -GAM 3.45 - -add six.py to solve compatability issues on OS X and Linux - -be conservative with password hashing to prevent timeouts - - If you see issues setting user passwords with GAM 3.44 or older, please upgrade to 3.45. - -GAM 3.44 - -"gam update cros assetid " allows updating of Chrome OS device Asset ID field. Thanks Erik Pitti! - -Windows versions of GAM now use pyinstaller instead of py2exe. - -upgraded versions of the googleapiclient and oauth2client libraries. - -"gam print cros" should produce a cleaner CSV now. - -GAM 3.43 - -Fix crash when authenticating GAM related to Short URLs - -minor fixes to Drive and Calendar commands - -catch unauthorized service account errors and offer nice instructions. - -execute bit for gam.py (Thanks daethnir) - -GAM 3.42 - -"gam show driveactivity" displays Drive activity for user - -"gam report tokens" displays user OAuth activity report - -"gam mobile action accountwipe" wipes account only on Android devices - -"gam update labels" removes Inbox/ prefix from label names - -upgrades to oauth2client and googleapiclient libraries which GAM depends on - -"gam show gmailprofile" shows Gmail mailbox details - -"gam license " commands to perform actions only on users with a given license - -bug fixes, lots of bug fixes - -GAM 3.41 - -fix Google servers not returning file size on audit export download - -soft fail on license change errors - -fix for gam info domain errors - -GAM 3.4 "Oktoberfest" - -Support for creating and setting custom user schemas http://goo.gl/M9rQrI - -End user view of print users and user info commands. - -fix updating name/description for groups - -fix groups sync commands - -gam print groups members no longer fails on zero member groups - -make sure downloaded Drive file names are safe for OS filenames. - -gam info domain should no longer crash on getting customer ID - -other minor bug fixes - -GAM 3.32 - -fix service account json files downloaded from new cloud console don't work with GAM - -use the new Gmail API for label command. Offers ability to rename labels and show/hide labels from the label and message list. - -copy Google drive files with commands like gam user update drivefile id copy - -print only one SKU for licenses with commands like gam print licenses sku vault - -short license names for Google Apps Message Security (gams), Google Apps Unlimtied (gau) and Google Vault Former Employee (vfe) - -GAM 3.31 - -New command "gam user delete aliases" clears all aliases for user. - -"gam update user email vfe" will rename user to vfe.oldname.XXXXX@olddomain.com - -fix delete photo command - -fix password update/create for Windows builds - -GAM 3.3 - -Major rewrites of "gam report" and "gam print users". Note that CSV headers have changed. Better performance and print users now supports "rich" fields like organization, phone, relations, etc. - -"gam report drive" now works for Google Apps Unlimited domains. - -"gam info user" now shows the user's licenses. Use "nolicenses" to prevent showing licenses. - -fix "gam print licenses" fails in large domains by reducing page size from 1000 to 100 - -GAM now sends a sha-512 salted hashed password on user create and update for better security. - -cleanup unused features of old GData library. - -fix for Drive file downloading default format. - -upgrade to httplib v0.9 which may help with httplib.BadStatus errors. - -GAM 3.21 - -Fix crash when attempting to perform Drive operations for users with Drive service disabled. - -Fix "gam info org" only prints first 100 users in org. - -GAM 3.2 - -New Google Drive GAM commands: http://goo.gl/l7F8hY - -New GAM batch and CSV commands allow bulk parallel GAM operations. Documentation coming at: http://goo.gl/2rsDnc - -GAM 3.04 - -New domain verification commands "gam create verify ", "gam update verify " and "gam info verify". - -"gam update group sync" commands should now only sync changes to the specified role (member, manager, owner). Other roles should be left alone. - -Fix: setting or viewing email signatures and vacation messages with unicode characters caused GAM to crash. - -Fix: issues printing out ASPs and backup codes if user had none set. - -Fix: "gam print orgs parent" always fails. - -GAM 3.03 - -New GAM user security commands allow management of OAuth tokens, 2SV backup codes and application specific passwords - -Google Vault license commands now work - -"gam update user password random" now resets user password to a very long, random string - -fixed updating location for Chrome devices - -fixed "gam update org" commands broken - -GAM 3.02 - -client_secrets.json is no longer shipped with GAM, you need to create your own with the instructions at http://goo.gl/QYaQ6R - -New "gam report logins" command to report on user login times and IP. - -Updated "gam report domain" command provides cleaner details of aggregate usage. - -"gam report admin" fixed. - -Fix "gam ou..." commands (they were hanging forever) - -Other minor cleanups and fixes. - -GAM 3.01 - -Fix gamcache errors on Windows by keeping cache filenames much shorter. - -add (back) support for setting/updating calendar colors - -add support for bulk updating users specified on the command like (gam update users "user1@domain.com user2@domain.com user3@domain.com"... OR gam update users user1@domain.com,user2@domain.com,user3@domain.com...) - -fix setting "gal off" during user creation. - -rewrite "gam info domain" to use new API library (should help with Unicode/UTF-8 errors) - -fix "show pop" and signature commands - -handle out of memory errors more gracefully - -GAM 3.0 - -Support for the Enterprise License API. Manage Drive storage and Google Coordinate licenses for users. - -Improved compatability with GAM commands from 2.55 and older. - -Fixed undelete user command. - -New "gam print group-members" command to print user membership of all groups. - -New "gam update user..." command to bulk modify settings for given users. For example, "gam all users update user changepassword on" will force password change for all users, 'gam group class-of-2013@acme.edu update user suspended on' will suspend all members of class-of-2013 group. - -Optimizations which should result in modest improvements to GAM startup time and performance. - -GAM 2.994 - -Rewritten "gam reports" commands. gam report users, gam report domain, gam report docs, gam report admin - -If CSV file uploaded to drive on "gam report" and "gam print" commands with the todrive parameter are more than 400,000 cells or 256 columns, don't convert to GDoc Spreadsheet. - -Remove old Admin Audit API scope (replaced by Reports API). - -new command: gam all users prism off - -GAM 2.992 - -Various minor fixes - -GAM 2.991 - -gam print commands now support a "todrive" argument. When specified, instead of displaying the CSV output locally (or piping it to a file), GAM will upload the CSV data to a Google Docs Spreadsheet owned by the admin you've authenticated as. The spreadsheet will be opened automatically or, if you've created nobrowser.txt, a URL will be shown. - -GAM should handle non-English input characters better. Commands like: "gam.py update user rpinaya lastname Piñaya" should work without issue on Windows. - -Improved errors in various places (less "explosions" more meaningful instructions. - -gam undelete user is fixed to always use the user's id rather than email address. If an email address is supplied, GAM converts it to a id before attempting to delete. If more than 1 deleted user exists with that address, GAM displays the options. - -GAM 2.99 - -Support for the newly announced Google Apps Admin SDK offering a richer feature set of management for your users, groups, aliases and other objects. - -Simplified OAuth 2.0 authentication - -Ability to manage Mobile and Chrome OS devices. - -Ability to add managers to groups - -Ability to manage group aliases - -Increased performance thanks to new Google API formats, caching, compression and partial update features. - -To many more features to list here! Download it now to see for yourself. - -GAM 2.55 - -Fix change in Google APIs broke "gam whatis" command. - -Fix change in Google APIs broke "gam info domain" command on CNAME Verification Status message. - -GAM 2.54 - -Fix a stupid bug that broke "gam print users" when used without additional attributes. - -Another fix for outbound gateway settings on "gam info domain" - -Get this whatsnew.txt doc up to date. - -GAM 2.53 - -Two new group settings, spam_moderation_level and include_in_global_address_list allow further customization of your Google Groups. - -Error reporting for mailbox delegation has been further improved, GAM does a better job of pinpointing why a delegation failed. - -Fixed updating and deleting domain and default users for calendar ACLs - -proper error handling for adding and removing group members and owners - -Fixed error on gam info domain caused by failure to retrieve outbound gateway settings. - -An EXPERIMENTAL 64-bit build of GAM for Windows is now available. I do not expect it will be any bit faster for most GAM commands since most delay - is due to network I/O. However, some GAM commands like "gam print users", when run against large domains (10,000+ users), use a large amount of memory - and resources due to result size. In these scenarios, the x64 build MIGHT prove faster. If you try the x64 build, please report how it worked back to - the mailing list. "It feels faster" is nice but hard numbers with details of what you did are better :-) Note that if you're using the Python source - build and your Python is 64-bit, you're already using 64-bit GAM :-) - -GAM 2.52 - -It's a dud! Major bug caused me to pull this version 10 minutes after release :-) - -GAM 2.51 - -New gam calendar wipe command allows clearing all data off a user's primary calendar - -create user and update user commands now support setting user's org. - -gam whatis command allows looking up an email address to determine if it's a user, alias or group. - -gam delete user no longer renames a user by default since undelete is now in CPanel. Added optional dorename parameter to force old renaming behavior. - -Fix issue that broke gam delete resource command - -GAM now offers to remember your client secret and key when entered the first time - -various other bug fixes - -GAM 2.5 - -GAM now handles and retries errors consistently and provides nice error messages. Long running GAM processes - like "gam all users" should be much stabler now. Death to the 1000/Unknown errors! - This involved some major changes to the Google API calls so if you run into problems, try - downgrading to 2.3.1 and see if they go away. Be sure to submit bug reports! - -GAM checks for updates - -New parameters for gam create user and gam update user - -New parameters for gam print group: owners, members and settings - -GAM now works for delegated admins with user read/create/update/delete API rights - -gam update group add owner now only adds the user as a group owner, not a member (Google Group member - and owner status are independant of each other) - -gam update group add member no longer revokes user's owner rights if they have them - -gam info group now shows owners who are not a member of the group - -gam now works around the group settings "Backend Error" by making an HTTP request to the groups website. - This workaround may cease to work if performed on more than a few hundred groups at a time. - -moving large numbers of users to an Organization is now more reliable and is performed 25 users at a time. - -gam print users aliases now makes only 1 API call to retrieve all user aliases - -New commands "gam oauth info" and "gam oauth revoke" allow further OAuth token management - -gam info domain now shows the unique customer id - -GAM 2.3.1 - -Fixes to add calendar command - -Allow updating and removal of special Calendar ACL users domain and default - -pop commands now work without supplying all arguments (defaults to enable for all mail and keep) - -New "file" argument for signature and vacation commands allows specifying a file with message content. - -"gam create group" now only requires group name argument, rest are optional. - -special user * (everyone in domain) can now be added to a group via GAM - -print groups, print resources, print aliases and print orgs commands now output proper CSV - -Dito company information now displayed on OAuth token create - -GAM 2.3 - - -GAM is now owned by Dito (www.ditoweb.com), the Google Apps Experts! See announcement and details at http://code.google.com/p/google-apps-manager - -New user profile photo management commands can update, get and delete user profile photos - -GAM now gracefully handles cross-domain mailbox delegations by using (or giving the delegate) an alias in the mailbox's domain. - -"gam user XXXX show delegates" now has optional argument "csv" to print existing delegations in CSV format - -GAM can now properly rename and delete long usernames by ensuring that the renamed user is max 64 characters in length - -"gam print groups" now has optional arguments nousermanagedgroups and onlyusermanagedgroups allowing user managed groups to be excluded from output or print user managed groups exclusively. - -GAM 2.2 - - -Update Calendar ACLs command, update user calendar settings command and ability to set calendar settings when subscribing user - -Delete Gmail labels command - -Fixes for *nix CSV formatting - -Fixes to make Windows and *nix generated oauth.txt files compatible - -"gam info user" now shows mailbox quota and user organization - -"gam update user" can now handle change of user's domain in renames. "gam multi" commands now fully deprecated. - -Fix reply_to and a few other group settings were never getting updated. - -"gam info group" now makes 3 efficient API calls rather than one per member/owner of the group greatly increasing performance with large groups - -GAM should do a better job of always printing out full email address instead of just username. If you see GAM reporting only the username and not the full email address, please report it as a bug. - -All OAuth scopes are now selected by default. - -GAM 2.1.1 - - -Fix to prevent unnecessary call to Groups Provisioning API when viewing detailed group settings - -should be show_in_group_directory not show_in_groups_directory. - -GAM 2.1 - - -New Reporting API Support allows you to pull 5 different daily reports: accounts, activity, disk_space, email_clients and summary. - -Fix for Adding calendars to a user's list of calendars. Bug in 2.0 meant calendar was always added to the calendar list of the admin who authorized GAM, not the target user. - -GAM now looks for an environment variable called OAUTHFILE. If it exists, GAM will use that file instead of oauth.txt for authentication. This allows admins of many Google Apps domains to switch quickly between domains. - -Fixes for many "gam print users" issues. Thanks to Craig Box for the patch. - -GAM 2.0 - --Group Settings commands allow you to update Google Group settings --Calendar commands allow you to grant access to calendars and modify user's list of calendars --Update Admin Settings like the logo, outbound gateway, email migration and more --OAuth is now the default authentication method. Support for username/password ClientLogin has been removed. --Vacation/Away messages can now have a start and end date. They can also be limited to within the domain only. --Further work to make all GAM commands multi-domain friendly. --Lot's more bugfixes! look at the Wiki pages for details - -GAM 1.9.1 - --"gam print postini" will print all of the Postini Batch commands necessary to "mirror" Google Apps email addresses - into a Postini standalone instance. - --"gam version" will print details about the version of GAM you are using. - -GAM 1.9 - "Baby Steps" - -GAM 1.9 is dedicated to David, my 13 month year old son. Whose just starting to step out into the world this week. - --whatnew.txt is new (is that an oxymoron?) - --Share or Hide users profile from autocomplete and contacts search. - "gam user jsmith show profile" - "gam user jsmith profile share" - "gam group asked-to-be-hidden profile unshare" - - Profile modifications only work with OAuth, not ClientLogin (username/password entered into GAM). - Since the profile API uses a scope GAM was not previously making use of, you'll need to re-run - "gam oauth request" to include the Profile API scope. - --Numerous actions can now be performed for all users in a given Organizational - Unit just like they can be for a group or all users. i.e. "gam ou Students webclips off". - --Provisioning API OAuth scope has been subdivided into user, group, alias and ou scopes - offering finer granularity. - --"gam all users" will now include all users across primary and secondary domains instead of just primary domain users. - --"gam info user" will show all email aliases for a user, not just those in the primary domain. - --"gam print users" with any extra arguments would fail, this should be fixed now. - --"gam info group" and "gam print groups" should no longer fail for groups with custom permissions. - -GAM 1.8 - --OAuth Support - GAM now supports OAuth Authentication. Instead of providing GAM your username and password, you grant GAM access to selected APIs from within your Google Account. This has a number of advantages: - -With OAuth GAM doesn't need to know your password. - -OAuth tokens don't expire, once you grant GAM OAuth access, GAM will have access until you revoke it within your Google account. - -OAuth has the concept of scopes, limiting the areas and services that access is granted to. This allows you to only provide GAM with the privileges it needs. - -More info about OAuth support is on it's way. But for now, you can try OAuth access by running "gam oauth request". - --The settings filter command http://code.google.com/p/google-apps-manager/wiki/ExamplesEmailSettings#Create_a_Filter now has more actions including forward, star, trash and never send to spam. - --Downloading Audit Exports now has partial resume support. GAM will not re-download files that already exist on the local drive. If a large export download fails you should delete the last file GAM was working on since it's incomplete and then restart the process, GAM will pick up with the last file. +GAM 3.61 + -Various fixes by Ross Scroggs for Domain API and Data Transfer commands + -Remove duplicate DoCreateDomain which broke "gam create domain" + +GAM 3.6 + -Change your primary domain! + -Transfer Google Drive and Google+ data between users. + -Domains API support includes ability to add, delete, update and print domains. + +GAM 3.51 + -delete or trash messages in mailboxes based on a Gmail search. + -create and delete course aliases with Classroom API. + -HUGE number of bug fixes, large and small by Ross Scroggs. Many thanks Ross! + +GAM 3.5 + -Support for the new Google Classroom API. + -create, update, info and delete courses + -add, remove and sync course teachers and students + -print courses and course participants + -Google CloudPrint API Support + -update, info, delete and report printers + -share, unshare and get ACLs for printers + -submit, cancel, report and delete print jobs + -Bug fixes and improvements to GAM batch commands + +GAM 3.45 + -add six.py to solve compatability issues on OS X and Linux + -be conservative with password hashing to prevent timeouts + + If you see issues setting user passwords with GAM 3.44 or older, please upgrade to 3.45. + +GAM 3.44 + -"gam update cros assetid " allows updating of Chrome OS device Asset ID field. Thanks Erik Pitti! + -Windows versions of GAM now use pyinstaller instead of py2exe. + -upgraded versions of the googleapiclient and oauth2client libraries. + -"gam print cros" should produce a cleaner CSV now. + +GAM 3.43 + -Fix crash when authenticating GAM related to Short URLs + -minor fixes to Drive and Calendar commands + -catch unauthorized service account errors and offer nice instructions. + -execute bit for gam.py (Thanks daethnir) + +GAM 3.42 + -"gam show driveactivity" displays Drive activity for user + -"gam report tokens" displays user OAuth activity report + -"gam mobile action accountwipe" wipes account only on Android devices + -"gam update labels" removes Inbox/ prefix from label names + -upgrades to oauth2client and googleapiclient libraries which GAM depends on + -"gam show gmailprofile" shows Gmail mailbox details + -"gam license " commands to perform actions only on users with a given license + -bug fixes, lots of bug fixes + +GAM 3.41 + -fix Google servers not returning file size on audit export download + -soft fail on license change errors + -fix for gam info domain errors + +GAM 3.4 "Oktoberfest" + -Support for creating and setting custom user schemas http://goo.gl/M9rQrI + -End user view of print users and user info commands. + -fix updating name/description for groups + -fix groups sync commands + -gam print groups members no longer fails on zero member groups + -make sure downloaded Drive file names are safe for OS filenames. + -gam info domain should no longer crash on getting customer ID + -other minor bug fixes + +GAM 3.32 + -fix service account json files downloaded from new cloud console don't work with GAM + -use the new Gmail API for label command. Offers ability to rename labels and show/hide labels from the label and message list. + -copy Google drive files with commands like gam user update drivefile id copy + -print only one SKU for licenses with commands like gam print licenses sku vault + -short license names for Google Apps Message Security (gams), Google Apps Unlimtied (gau) and Google Vault Former Employee (vfe) + +GAM 3.31 + -New command "gam user delete aliases" clears all aliases for user. + -"gam update user email vfe" will rename user to vfe.oldname.XXXXX@olddomain.com + -fix delete photo command + -fix password update/create for Windows builds + +GAM 3.3 + -Major rewrites of "gam report" and "gam print users". Note that CSV headers have changed. Better performance and print users now supports "rich" fields like organization, phone, relations, etc. + -"gam report drive" now works for Google Apps Unlimited domains. + -"gam info user" now shows the user's licenses. Use "nolicenses" to prevent showing licenses. + -fix "gam print licenses" fails in large domains by reducing page size from 1000 to 100 + -GAM now sends a sha-512 salted hashed password on user create and update for better security. + -cleanup unused features of old GData library. + -fix for Drive file downloading default format. + -upgrade to httplib v0.9 which may help with httplib.BadStatus errors. + +GAM 3.21 + -Fix crash when attempting to perform Drive operations for users with Drive service disabled. + -Fix "gam info org" only prints first 100 users in org. + +GAM 3.2 + -New Google Drive GAM commands: http://goo.gl/l7F8hY + -New GAM batch and CSV commands allow bulk parallel GAM operations. Documentation coming at: http://goo.gl/2rsDnc + +GAM 3.04 + -New domain verification commands "gam create verify ", "gam update verify " and "gam info verify". + -"gam update group sync" commands should now only sync changes to the specified role (member, manager, owner). Other roles should be left alone. + -Fix: setting or viewing email signatures and vacation messages with unicode characters caused GAM to crash. + -Fix: issues printing out ASPs and backup codes if user had none set. + -Fix: "gam print orgs parent" always fails. + +GAM 3.03 + -New GAM user security commands allow management of OAuth tokens, 2SV backup codes and application specific passwords + -Google Vault license commands now work + -"gam update user password random" now resets user password to a very long, random string + -fixed updating location for Chrome devices + -fixed "gam update org" commands broken + +GAM 3.02 + -client_secrets.json is no longer shipped with GAM, you need to create your own with the instructions at http://goo.gl/QYaQ6R + -New "gam report logins" command to report on user login times and IP. + -Updated "gam report domain" command provides cleaner details of aggregate usage. + -"gam report admin" fixed. + -Fix "gam ou..." commands (they were hanging forever) + -Other minor cleanups and fixes. + +GAM 3.01 + -Fix gamcache errors on Windows by keeping cache filenames much shorter. + -add (back) support for setting/updating calendar colors + -add support for bulk updating users specified on the command like (gam update users "user1@domain.com user2@domain.com user3@domain.com"... OR gam update users user1@domain.com,user2@domain.com,user3@domain.com...) + -fix setting "gal off" during user creation. + -rewrite "gam info domain" to use new API library (should help with Unicode/UTF-8 errors) + -fix "show pop" and signature commands + -handle out of memory errors more gracefully + +GAM 3.0 + -Support for the Enterprise License API. Manage Drive storage and Google Coordinate licenses for users. + -Improved compatability with GAM commands from 2.55 and older. + -Fixed undelete user command. + -New "gam print group-members" command to print user membership of all groups. + -New "gam update user..." command to bulk modify settings for given users. For example, "gam all users update user changepassword on" will force password change for all users, 'gam group class-of-2013@acme.edu update user suspended on' will suspend all members of class-of-2013 group. + -Optimizations which should result in modest improvements to GAM startup time and performance. + +GAM 2.994 + -Rewritten "gam reports" commands. gam report users, gam report domain, gam report docs, gam report admin + -If CSV file uploaded to drive on "gam report" and "gam print" commands with the todrive parameter are more than 400,000 cells or 256 columns, don't convert to GDoc Spreadsheet. + -Remove old Admin Audit API scope (replaced by Reports API). + -new command: gam all users prism off + +GAM 2.992 + -Various minor fixes + +GAM 2.991 + -gam print commands now support a "todrive" argument. When specified, instead of displaying the CSV output locally (or piping it to a file), GAM will upload the CSV data to a Google Docs Spreadsheet owned by the admin you've authenticated as. The spreadsheet will be opened automatically or, if you've created nobrowser.txt, a URL will be shown. + -GAM should handle non-English input characters better. Commands like: "gam.py update user rpinaya lastname Piñaya" should work without issue on Windows. + -Improved errors in various places (less "explosions" more meaningful instructions. + -gam undelete user is fixed to always use the user's id rather than email address. If an email address is supplied, GAM converts it to a id before attempting to delete. If more than 1 deleted user exists with that address, GAM displays the options. + +GAM 2.99 + -Support for the newly announced Google Apps Admin SDK offering a richer feature set of management for your users, groups, aliases and other objects. + -Simplified OAuth 2.0 authentication + -Ability to manage Mobile and Chrome OS devices. + -Ability to add managers to groups + -Ability to manage group aliases + -Increased performance thanks to new Google API formats, caching, compression and partial update features. + -To many more features to list here! Download it now to see for yourself. + +GAM 2.55 + -Fix change in Google APIs broke "gam whatis" command. + -Fix change in Google APIs broke "gam info domain" command on CNAME Verification Status message. + +GAM 2.54 + -Fix a stupid bug that broke "gam print users" when used without additional attributes. + -Another fix for outbound gateway settings on "gam info domain" + -Get this whatsnew.txt doc up to date. + +GAM 2.53 + -Two new group settings, spam_moderation_level and include_in_global_address_list allow further customization of your Google Groups. + -Error reporting for mailbox delegation has been further improved, GAM does a better job of pinpointing why a delegation failed. + -Fixed updating and deleting domain and default users for calendar ACLs + -proper error handling for adding and removing group members and owners + -Fixed error on gam info domain caused by failure to retrieve outbound gateway settings. + -An EXPERIMENTAL 64-bit build of GAM for Windows is now available. I do not expect it will be any bit faster for most GAM commands since most delay + is due to network I/O. However, some GAM commands like "gam print users", when run against large domains (10,000+ users), use a large amount of memory + and resources due to result size. In these scenarios, the x64 build MIGHT prove faster. If you try the x64 build, please report how it worked back to + the mailing list. "It feels faster" is nice but hard numbers with details of what you did are better :-) Note that if you're using the Python source + build and your Python is 64-bit, you're already using 64-bit GAM :-) + +GAM 2.52 + -It's a dud! Major bug caused me to pull this version 10 minutes after release :-) + +GAM 2.51 + -New gam calendar wipe command allows clearing all data off a user's primary calendar + -create user and update user commands now support setting user's org. + -gam whatis command allows looking up an email address to determine if it's a user, alias or group. + -gam delete user no longer renames a user by default since undelete is now in CPanel. Added optional dorename parameter to force old renaming behavior. + -Fix issue that broke gam delete resource command + -GAM now offers to remember your client secret and key when entered the first time + -various other bug fixes + +GAM 2.5 + -GAM now handles and retries errors consistently and provides nice error messages. Long running GAM processes + like "gam all users" should be much stabler now. Death to the 1000/Unknown errors! + This involved some major changes to the Google API calls so if you run into problems, try + downgrading to 2.3.1 and see if they go away. Be sure to submit bug reports! + -GAM checks for updates + -New parameters for gam create user and gam update user + -New parameters for gam print group: owners, members and settings + -GAM now works for delegated admins with user read/create/update/delete API rights + -gam update group add owner now only adds the user as a group owner, not a member (Google Group member + and owner status are independant of each other) + -gam update group add member no longer revokes user's owner rights if they have them + -gam info group now shows owners who are not a member of the group + -gam now works around the group settings "Backend Error" by making an HTTP request to the groups website. + This workaround may cease to work if performed on more than a few hundred groups at a time. + -moving large numbers of users to an Organization is now more reliable and is performed 25 users at a time. + -gam print users aliases now makes only 1 API call to retrieve all user aliases + -New commands "gam oauth info" and "gam oauth revoke" allow further OAuth token management + -gam info domain now shows the unique customer id + +GAM 2.3.1 + -Fixes to add calendar command + -Allow updating and removal of special Calendar ACL users domain and default + -pop commands now work without supplying all arguments (defaults to enable for all mail and keep) + -New "file" argument for signature and vacation commands allows specifying a file with message content. + -"gam create group" now only requires group name argument, rest are optional. + -special user * (everyone in domain) can now be added to a group via GAM + -print groups, print resources, print aliases and print orgs commands now output proper CSV + -Dito company information now displayed on OAuth token create + +GAM 2.3 + + -GAM is now owned by Dito (www.ditoweb.com), the Google Apps Experts! See announcement and details at http://code.google.com/p/google-apps-manager + -New user profile photo management commands can update, get and delete user profile photos + -GAM now gracefully handles cross-domain mailbox delegations by using (or giving the delegate) an alias in the mailbox's domain. + -"gam user XXXX show delegates" now has optional argument "csv" to print existing delegations in CSV format + -GAM can now properly rename and delete long usernames by ensuring that the renamed user is max 64 characters in length + -"gam print groups" now has optional arguments nousermanagedgroups and onlyusermanagedgroups allowing user managed groups to be excluded from output or print user managed groups exclusively. + +GAM 2.2 + + -Update Calendar ACLs command, update user calendar settings command and ability to set calendar settings when subscribing user + -Delete Gmail labels command + -Fixes for *nix CSV formatting + -Fixes to make Windows and *nix generated oauth.txt files compatible + -"gam info user" now shows mailbox quota and user organization + -"gam update user" can now handle change of user's domain in renames. "gam multi" commands now fully deprecated. + -Fix reply_to and a few other group settings were never getting updated. + -"gam info group" now makes 3 efficient API calls rather than one per member/owner of the group greatly increasing performance with large groups + -GAM should do a better job of always printing out full email address instead of just username. If you see GAM reporting only the username and not the full email address, please report it as a bug. + -All OAuth scopes are now selected by default. + +GAM 2.1.1 + + -Fix to prevent unnecessary call to Groups Provisioning API when viewing detailed group settings + -should be show_in_group_directory not show_in_groups_directory. + +GAM 2.1 + + -New Reporting API Support allows you to pull 5 different daily reports: accounts, activity, disk_space, email_clients and summary. + -Fix for Adding calendars to a user's list of calendars. Bug in 2.0 meant calendar was always added to the calendar list of the admin who authorized GAM, not the target user. + -GAM now looks for an environment variable called OAUTHFILE. If it exists, GAM will use that file instead of oauth.txt for authentication. This allows admins of many Google Apps domains to switch quickly between domains. + -Fixes for many "gam print users" issues. Thanks to Craig Box for the patch. + +GAM 2.0 + +-Group Settings commands allow you to update Google Group settings +-Calendar commands allow you to grant access to calendars and modify user's list of calendars +-Update Admin Settings like the logo, outbound gateway, email migration and more +-OAuth is now the default authentication method. Support for username/password ClientLogin has been removed. +-Vacation/Away messages can now have a start and end date. They can also be limited to within the domain only. +-Further work to make all GAM commands multi-domain friendly. +-Lot's more bugfixes! look at the Wiki pages for details + +GAM 1.9.1 + +-"gam print postini" will print all of the Postini Batch commands necessary to "mirror" Google Apps email addresses + into a Postini standalone instance. + +-"gam version" will print details about the version of GAM you are using. + +GAM 1.9 - "Baby Steps" + +GAM 1.9 is dedicated to David, my 13 month year old son. Whose just starting to step out into the world this week. + +-whatnew.txt is new (is that an oxymoron?) + +-Share or Hide users profile from autocomplete and contacts search. + "gam user jsmith show profile" + "gam user jsmith profile share" + "gam group asked-to-be-hidden profile unshare" + + Profile modifications only work with OAuth, not ClientLogin (username/password entered into GAM). + Since the profile API uses a scope GAM was not previously making use of, you'll need to re-run + "gam oauth request" to include the Profile API scope. + +-Numerous actions can now be performed for all users in a given Organizational + Unit just like they can be for a group or all users. i.e. "gam ou Students webclips off". + +-Provisioning API OAuth scope has been subdivided into user, group, alias and ou scopes + offering finer granularity. + +-"gam all users" will now include all users across primary and secondary domains instead of just primary domain users. + +-"gam info user" will show all email aliases for a user, not just those in the primary domain. + +-"gam print users" with any extra arguments would fail, this should be fixed now. + +-"gam info group" and "gam print groups" should no longer fail for groups with custom permissions. + +GAM 1.8 + +-OAuth Support - GAM now supports OAuth Authentication. Instead of providing GAM your username and password, you grant GAM access to selected APIs from within your Google Account. This has a number of advantages: + -With OAuth GAM doesn't need to know your password. + -OAuth tokens don't expire, once you grant GAM OAuth access, GAM will have access until you revoke it within your Google account. + -OAuth has the concept of scopes, limiting the areas and services that access is granted to. This allows you to only provide GAM with the privileges it needs. + -More info about OAuth support is on it's way. But for now, you can try OAuth access by running "gam oauth request". + +-The settings filter command http://code.google.com/p/google-apps-manager/wiki/ExamplesEmailSettings#Create_a_Filter now has more actions including forward, star, trash and never send to spam. + +-Downloading Audit Exports now has partial resume support. GAM will not re-download files that already exist on the local drive. If a large export download fails you should delete the last file GAM was working on since it's incomplete and then restart the process, GAM will pick up with the last file.