mirror of
https://github.com/GAM-team/GAM.git
synced 2026-06-04 06:11:39 +00:00
Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbb70421ed | ||
|
|
7a2785ba5c | ||
|
|
2c15230756 | ||
|
|
232755590d | ||
|
|
ddd040e395 | ||
|
|
a9dc255979 | ||
|
|
785073eb87 | ||
|
|
368a6d1217 | ||
|
|
13d2f9dd96 | ||
|
|
e85e54b86a | ||
|
|
e39d441d01 | ||
|
|
f6f15c8801 | ||
|
|
0dc8accb64 | ||
|
|
79964892f6 | ||
|
|
b648ddbfdf | ||
|
|
cab068357a | ||
|
|
acb4b39953 | ||
|
|
74481b1c31 | ||
|
|
aa9cd3f1ca | ||
|
|
7f67a3043a | ||
|
|
734413266b | ||
|
|
159d4cf085 | ||
|
|
4b380be637 | ||
|
|
0f84ee1e07 | ||
|
|
38df869afc | ||
|
|
c05c2ffaa2 | ||
|
|
bb86db3cd3 | ||
|
|
9e2e9632c2 | ||
|
|
2b686ebe74 | ||
|
|
15043d1de8 | ||
|
|
e00151ef39 | ||
|
|
160ea6aa5b | ||
|
|
7e3297e8c7 | ||
|
|
880a9c8939 | ||
|
|
64f9cbd54f | ||
|
|
6b97662f8a | ||
|
|
71f37acdf4 | ||
|
|
a58c300e84 | ||
|
|
b1ce6544f5 | ||
|
|
d5daf24c15 | ||
|
|
9b9aea9841 | ||
|
|
1145faef56 | ||
|
|
e42ca916fe | ||
|
|
24611614d6 | ||
|
|
6df45551e5 | ||
|
|
7c211c664f | ||
|
|
f07a2ef2be | ||
|
|
3df6d1f833 | ||
|
|
f6396d5e4a | ||
|
|
8db8c48fd9 | ||
|
|
8cc4710874 | ||
|
|
d42f4d764a | ||
|
|
21715f079b | ||
|
|
b345cb063f | ||
|
|
d6fdaa2874 | ||
|
|
daf2735eb3 | ||
|
|
68ad322f41 | ||
|
|
f688404ac6 | ||
|
|
13a9024dd1 | ||
|
|
2fdfc3750d | ||
|
|
9246aed660 | ||
|
|
0efb736b42 | ||
|
|
ef7fcb2114 | ||
|
|
52f4c049e1 | ||
|
|
ee00a41ff9 | ||
|
|
289fda94df | ||
|
|
49d1845129 | ||
|
|
d850f5575b | ||
|
|
1f2ffcf97a | ||
|
|
4ea63c3167 | ||
|
|
f2887abb49 | ||
|
|
3ada129e7f | ||
|
|
6c3a0e2b71 | ||
|
|
457feac4ac | ||
|
|
45728fbbda | ||
|
|
0d507855bd | ||
|
|
5fa2f3d955 | ||
|
|
bfc734138d | ||
|
|
fdfa830aa0 | ||
|
|
d78ea8efeb | ||
|
|
d47f5967e9 | ||
|
|
2debf9507a | ||
|
|
e527cd42a1 | ||
|
|
38399c0e1e | ||
|
|
43782ca3f8 | ||
|
|
b5f911e259 | ||
|
|
d4cad7a242 | ||
|
|
9ba732e0bf | ||
|
|
b6caa8a5ba | ||
|
|
cca9684ffb | ||
|
|
c00259a5b6 | ||
|
|
455730dad8 | ||
|
|
b17b80ee12 | ||
|
|
4fadf68da4 | ||
|
|
fb9aebf123 | ||
|
|
087c6775e3 | ||
|
|
a3c509ce61 | ||
|
|
6b0fce21a5 | ||
|
|
7b8b4674e7 | ||
|
|
4956873357 | ||
|
|
4dd1d6a244 | ||
|
|
65367947e0 | ||
|
|
a83414f831 | ||
|
|
bba894bdc3 | ||
|
|
190c4f212d | ||
|
|
d8a78d96ae | ||
|
|
1d30eb7d91 | ||
|
|
ac86758e79 | ||
|
|
afee6c32a3 | ||
|
|
7412236679 | ||
|
|
3ef433687a | ||
|
|
82d43d0b62 | ||
|
|
bf31e72384 | ||
|
|
2a37589a9f | ||
|
|
cafa01248a | ||
|
|
5ab14fef05 | ||
|
|
6b6ada5b2c | ||
|
|
18420275af | ||
|
|
8f283acf66 | ||
|
|
f27df74339 | ||
|
|
ca059a62a6 | ||
|
|
6dae2302c0 | ||
|
|
bae5f20ec4 | ||
|
|
51a4d92a90 | ||
|
|
d527f4104f | ||
|
|
2d74916ca5 | ||
|
|
3fcde95fe8 | ||
|
|
2235c10df7 | ||
|
|
327e09291b | ||
|
|
1dd36424be | ||
|
|
ade2d0ae54 | ||
|
|
ac3dbd25f3 | ||
|
|
2e6811d2d4 | ||
|
|
61a9d0c0a6 | ||
|
|
4cc775bcae | ||
|
|
96dfa52dba | ||
|
|
1f1329c536 | ||
|
|
3bb54f875d | ||
|
|
84b6c1cb87 | ||
|
|
4f4bb316d0 | ||
|
|
fe6430edc6 | ||
|
|
6c421de8c4 | ||
|
|
c04ae91dc5 | ||
|
|
14bc340e56 | ||
|
|
6fd107c230 | ||
|
|
5f2c2103a5 | ||
|
|
a06828dbcf | ||
|
|
b260cb8f50 | ||
|
|
2fc3355388 | ||
|
|
24d5a169f6 | ||
|
|
2bc0dd5fbe | ||
|
|
d76e5008a7 | ||
|
|
21f01757a3 | ||
|
|
4da936344f | ||
|
|
05920cc7d7 | ||
|
|
edb17ad06e | ||
|
|
ab6f8fa7bf | ||
|
|
b822608b15 | ||
|
|
7a9bda9b1b | ||
|
|
3edfce202f | ||
|
|
c5b4a822d9 | ||
|
|
e3ae862732 | ||
|
|
74b1127f6e | ||
|
|
2c3f12b38c | ||
|
|
94ee718aa9 | ||
|
|
f34620aa73 | ||
|
|
a43bb56a43 | ||
|
|
a4ed95b81b | ||
|
|
ddd8348bdd | ||
|
|
d32095f3fe | ||
|
|
a958bf8be7 | ||
|
|
2d643a551c | ||
|
|
b16d75ec43 | ||
|
|
f40af555c3 | ||
|
|
c82672d77b | ||
|
|
867488bf77 | ||
|
|
9c485334f1 | ||
|
|
04ff83fc2d | ||
|
|
d61e2751ef | ||
|
|
87b64572db | ||
|
|
50a33a5083 | ||
|
|
2dc72ab262 | ||
|
|
6dec0ea0f1 | ||
|
|
fdc4e867c2 | ||
|
|
525731fe33 | ||
|
|
66d86b8d4d | ||
|
|
2443d5d1cf | ||
|
|
8863e7337e | ||
|
|
27b31ff1fb | ||
|
|
58335025c4 | ||
|
|
c1225178d6 | ||
|
|
8b19040e45 | ||
|
|
6ba62b66b4 | ||
|
|
e1bd7d7ae9 | ||
|
|
c9b2c1d8d6 | ||
|
|
1ae2b960c7 | ||
|
|
0c2d4ab5cb | ||
|
|
b9460cbcea | ||
|
|
886a26cc5d | ||
|
|
94dcd98e8d | ||
|
|
c3038807b9 | ||
|
|
7acb8a9e85 | ||
|
|
2d47622a0e | ||
|
|
bbfe5d36e8 | ||
|
|
0ecb732d60 | ||
|
|
8b5ac30030 | ||
|
|
0c7bf10355 | ||
|
|
319273eb03 | ||
|
|
199c49ff5e | ||
|
|
8d967b1125 | ||
|
|
277b5ac261 | ||
|
|
0b035deff0 | ||
|
|
73c3cb013f | ||
|
|
f2b2c90586 | ||
|
|
2c21aac0d6 |
55
README.md
55
README.md
@@ -1,66 +1,25 @@
|
||||
Dito GAM
|
||||
GAM
|
||||
============================
|
||||
GAM is a free, open source command line tool for
|
||||
Google Apps Administrators to efficiently manage
|
||||
domain and user settings quickly and easily. GAM has support
|
||||
for many features, such as
|
||||
|
||||
* creating, deleting, and updating users, aliases, groups,
|
||||
organizations, and resource calendars
|
||||
* modifying user email settings such as IMAP, signatures,
|
||||
vacation messages, profile sharing, email forwarding,
|
||||
send as address, labels, and features.
|
||||
* delegating mailboxes and calendars to other users
|
||||
* modifying calendar access rights for users and resource calendars.
|
||||
* auditing user accounts and mailboes
|
||||
* monitoring incoming and outgoing email
|
||||
* generating detailed reports for users, groups, resources,
|
||||
account activity, email clients, and quotas.
|
||||
GAM is a command line tool for Google Apps Administrators to manage domain and user settings quickly and easily.
|
||||
|
||||
Downloads
|
||||
---------
|
||||
You can download current GAM from
|
||||
the [GitHub Releases] page.
|
||||
You can download the current GAM release from the [GitHub Releases] page.
|
||||
|
||||
Documentation
|
||||
------------------
|
||||
The GAM documentation is currently hosted in the [GitHub Wiki]
|
||||
The GAM documentation is hosted in the [GitHub Wiki]
|
||||
|
||||
Mailing List / Discussion group
|
||||
-------------------------------
|
||||
The GAM mailing list / discussion group is hosted
|
||||
on [Google Groups]. You can join the list and interact
|
||||
via email, or just post from the web itself.
|
||||
|
||||
Source Repository
|
||||
-----------------
|
||||
|
||||
The official GAM source repository is on [GitHub].
|
||||
The GAM mailing list / discussion group is hosted on [Google Groups]. You can join the list and interact via email, or just post from the web itself.
|
||||
|
||||
Author
|
||||
------
|
||||
|
||||
GAM is maintained by <a href="mailto:jay0lee@gmail.com">Jay Lee</a>.
|
||||
|
||||
Thanks To
|
||||
---------
|
||||
|
||||
GAM is made possible and maintained by the work of Dito.
|
||||
Who is Dito?
|
||||
|
||||
*Dito is solely focused on moving organizations to Google's
|
||||
cloud. After hundreds of successful deployments over the
|
||||
last 5 years, we have gained notoriety for our complete
|
||||
understanding of the platform, our change management &
|
||||
training ability, and our rock-star deployment engineers.
|
||||
We are known worldwide as the Google Apps Experts.*
|
||||
|
||||
Need a Google Apps Expert?
|
||||
[Contact Dito](http://ditoweb.com/contact), which offers
|
||||
[free premium GAM support](http://www.ditoweb.com/dito-gam)
|
||||
for domains that sign up through Dito.
|
||||
|
||||
[GAM release]: https://git.io/gamreleases
|
||||
[GitHub Releases]: https://github.com/jay0lee/GAM/releases
|
||||
[GitHub]: https://github.com/jay0lee/GAM/
|
||||
[GitHub]: https://github.com/jay0lee/GAM/tree/master
|
||||
[GitHub Wiki]: https://github.com/jay0lee/GAM/wiki/
|
||||
[Google Groups]: http://groups.google.com/group/google-apps-manager
|
||||
|
||||
3338
cacert.pem
3338
cacert.pem
File diff suppressed because it is too large
Load Diff
@@ -1,71 +0,0 @@
|
||||
# Copyright (C) 2008 Google, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Allow Google Apps domain administrators to create/modify/delete resource calendars.
|
||||
|
||||
ResCalService: Interact with Resource Calendars."""
|
||||
|
||||
__author__ = 'jlee@pbu.edu'
|
||||
|
||||
import gdata.apps
|
||||
import gdata.apps.service
|
||||
import gdata.service
|
||||
|
||||
class ResCalService(gdata.apps.service.PropertyService):
|
||||
"""Client for the Google Apps Resource Calendar service."""
|
||||
|
||||
def _serviceUrl(self, domain=None):
|
||||
if domain is None:
|
||||
domain = self.domain
|
||||
return '/a/feeds/calendar/resource/2.0/%s' % domain
|
||||
|
||||
def CreateResourceCalendar(self, id, common_name, description=None, type=None):
|
||||
|
||||
uri = self._serviceUrl()
|
||||
properties = {}
|
||||
properties['resourceId'] = id
|
||||
properties['resourceCommonName'] = common_name
|
||||
if description != None:
|
||||
properties['resourceDescription'] = description
|
||||
if type != None:
|
||||
properties['resourceType'] = type
|
||||
return self._PostProperties(uri, properties)
|
||||
|
||||
def RetrieveResourceCalendar(self, id):
|
||||
|
||||
uri = self._serviceUrl()+'/'+id
|
||||
return self._GetProperties(uri)
|
||||
|
||||
def RetrieveAllResourceCalendars(self):
|
||||
|
||||
uri = self._serviceUrl()+'/'
|
||||
return self._GetPropertiesList(uri)
|
||||
|
||||
def UpdateResourceCalendar(self, id, common_name=None, description=None, type=None):
|
||||
|
||||
uri = self._serviceUrl()+'/'+id
|
||||
properties = {}
|
||||
properties['resourceId'] = id
|
||||
if common_name != None:
|
||||
properties['resourceCommonName'] = common_name
|
||||
if description != None:
|
||||
properties['resourceDescription'] = description
|
||||
if type != None:
|
||||
properties['resourceType'] = type
|
||||
return self._PutProperties(uri, properties)
|
||||
|
||||
def DeleteResourceCalendar(self, id):
|
||||
|
||||
uri = self._serviceUrl()+'/'+id
|
||||
return self._DeleteProperties(uri)
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Client library for using OAuth2, especially with Google APIs."""
|
||||
|
||||
__version__ = '1.4.7'
|
||||
|
||||
GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
|
||||
GOOGLE_DEVICE_URI = 'https://accounts.google.com/o/oauth2/device/code'
|
||||
GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
|
||||
GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
|
||||
@@ -1,987 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google App Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google App Engine.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import cgi
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import threading
|
||||
|
||||
import httplib2
|
||||
|
||||
from google.appengine.api import app_identity
|
||||
from google.appengine.api import memcache
|
||||
from google.appengine.api import users
|
||||
from google.appengine.ext import db
|
||||
from google.appengine.ext import webapp
|
||||
from google.appengine.ext.webapp.util import login_required
|
||||
from google.appengine.ext.webapp.util import run_wsgi_app
|
||||
from oauth2client import GOOGLE_AUTH_URI
|
||||
from oauth2client import GOOGLE_REVOKE_URI
|
||||
from oauth2client import GOOGLE_TOKEN_URI
|
||||
from oauth2client import clientsecrets
|
||||
from oauth2client import util
|
||||
from oauth2client import xsrfutil
|
||||
from oauth2client.client import AccessTokenRefreshError
|
||||
from oauth2client.client import AssertionCredentials
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Flow
|
||||
from oauth2client.client import OAuth2WebServerFlow
|
||||
from oauth2client.client import Storage
|
||||
|
||||
# TODO(dhermes): Resolve import issue.
|
||||
# This is a temporary fix for a Google internal issue.
|
||||
try:
|
||||
from google.appengine.ext import ndb
|
||||
except ImportError:
|
||||
ndb = None
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
|
||||
|
||||
XSRF_MEMCACHE_ID = 'xsrf_secret_key'
|
||||
|
||||
|
||||
def _safe_html(s):
|
||||
"""Escape text to make it safe to display.
|
||||
|
||||
Args:
|
||||
s: string, The text to escape.
|
||||
|
||||
Returns:
|
||||
The escaped text as a string.
|
||||
"""
|
||||
return cgi.escape(s, quote=1).replace("'", ''')
|
||||
|
||||
|
||||
class InvalidClientSecretsError(Exception):
|
||||
"""The client_secrets.json file is malformed or missing required fields."""
|
||||
|
||||
|
||||
class InvalidXsrfTokenError(Exception):
|
||||
"""The XSRF token is invalid or expired."""
|
||||
|
||||
|
||||
class SiteXsrfSecretKey(db.Model):
|
||||
"""Storage for the sites XSRF secret key.
|
||||
|
||||
There will only be one instance stored of this model, the one used for the
|
||||
site.
|
||||
"""
|
||||
secret = db.StringProperty()
|
||||
|
||||
if ndb is not None:
|
||||
class SiteXsrfSecretKeyNDB(ndb.Model):
|
||||
"""NDB Model for storage for the sites XSRF secret key.
|
||||
|
||||
Since this model uses the same kind as SiteXsrfSecretKey, it can be used
|
||||
interchangeably. This simply provides an NDB model for interacting with the
|
||||
same data the DB model interacts with.
|
||||
|
||||
There should only be one instance stored of this model, the one used for the
|
||||
site.
|
||||
"""
|
||||
secret = ndb.StringProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'SiteXsrfSecretKey'
|
||||
|
||||
|
||||
def _generate_new_xsrf_secret_key():
|
||||
"""Returns a random XSRF secret key.
|
||||
"""
|
||||
return os.urandom(16).encode("hex")
|
||||
|
||||
|
||||
def xsrf_secret_key():
|
||||
"""Return the secret key for use for XSRF protection.
|
||||
|
||||
If the Site entity does not have a secret key, this method will also create
|
||||
one and persist it.
|
||||
|
||||
Returns:
|
||||
The secret key.
|
||||
"""
|
||||
secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
if not secret:
|
||||
# Load the one and only instance of SiteXsrfSecretKey.
|
||||
model = SiteXsrfSecretKey.get_or_insert(key_name='site')
|
||||
if not model.secret:
|
||||
model.secret = _generate_new_xsrf_secret_key()
|
||||
model.put()
|
||||
secret = model.secret
|
||||
memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
|
||||
|
||||
return str(secret)
|
||||
|
||||
|
||||
class AppAssertionCredentials(AssertionCredentials):
|
||||
"""Credentials object for App Engine Assertion Grants
|
||||
|
||||
This object will allow an App Engine application to identify itself to Google
|
||||
and other OAuth 2.0 servers that can verify assertions. It can be used for the
|
||||
purpose of accessing data stored under an account assigned to the App Engine
|
||||
application itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it represents
|
||||
a two legged flow, and therefore has all of the required information to
|
||||
generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
**kwargs: optional keyword args, including:
|
||||
service_account_id: service account id of the application. If None or
|
||||
unspecified, the default service account for the app is used.
|
||||
"""
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self._kwargs = kwargs
|
||||
self.service_account_id = kwargs.get('service_account_id', None)
|
||||
|
||||
# Assertion type is no longer used, but still in the parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(json_data)
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Since the underlying App Engine app_identity implementation does its own
|
||||
caching we can skip all the storage hoops and just to a refresh using the
|
||||
API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method signature of
|
||||
httplib2.Http.request, used to make the refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
try:
|
||||
scopes = self.scope.split()
|
||||
(token, _) = app_identity.get_access_token(
|
||||
scopes, service_account_id=self.service_account_id)
|
||||
except app_identity.Error as e:
|
||||
raise AccessTokenRefreshError(str(e))
|
||||
self.access_token = token
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError('Cannot serialize credentials for AppEngine.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self._kwargs)
|
||||
|
||||
|
||||
class FlowProperty(db.Property):
|
||||
"""App Engine datastore Property for Flow.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = Flow
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
flow = super(FlowProperty,
|
||||
self).get_value_for_datastore(model_instance)
|
||||
return db.Blob(pickle.dumps(flow))
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
return pickle.loads(value)
|
||||
|
||||
def validate(self, value):
|
||||
if value is not None and not isinstance(value, Flow):
|
||||
raise db.BadValueError('Property %s must be convertible '
|
||||
'to a FlowThreeLegged instance (%s)' %
|
||||
(self.name, value))
|
||||
return super(FlowProperty, self).validate(value)
|
||||
|
||||
def empty(self, value):
|
||||
return not value
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
class FlowNDBProperty(ndb.PickleProperty):
|
||||
"""App Engine NDB datastore Property for Flow.
|
||||
|
||||
Serves the same purpose as the DB FlowProperty, but for NDB models. Since
|
||||
PickleProperty inherits from BlobProperty, the underlying representation of
|
||||
the data in the datastore will be the same as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of an
|
||||
oauth2client.Flow
|
||||
"""
|
||||
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper Flow object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Flow.
|
||||
"""
|
||||
logger.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, Flow):
|
||||
raise TypeError('Property %s must be convertible to a flow '
|
||||
'instance; received: %s.' % (self._name, value))
|
||||
|
||||
|
||||
class CredentialsProperty(db.Property):
|
||||
"""App Engine datastore Property for Credentials.
|
||||
|
||||
Utility property that allows easy storage and retrieval of
|
||||
oath2client.Credentials
|
||||
"""
|
||||
|
||||
# Tell what the user type is.
|
||||
data_type = Credentials
|
||||
|
||||
# For writing to datastore.
|
||||
def get_value_for_datastore(self, model_instance):
|
||||
logger.info("get: Got type " + str(type(model_instance)))
|
||||
cred = super(CredentialsProperty,
|
||||
self).get_value_for_datastore(model_instance)
|
||||
if cred is None:
|
||||
cred = ''
|
||||
else:
|
||||
cred = cred.to_json()
|
||||
return db.Blob(cred)
|
||||
|
||||
# For reading from datastore.
|
||||
def make_value_from_datastore(self, value):
|
||||
logger.info("make: Got type " + str(type(value)))
|
||||
if value is None:
|
||||
return None
|
||||
if len(value) == 0:
|
||||
return None
|
||||
try:
|
||||
credentials = Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
def validate(self, value):
|
||||
value = super(CredentialsProperty, self).validate(value)
|
||||
logger.info("validate: Got type " + str(type(value)))
|
||||
if value is not None and not isinstance(value, Credentials):
|
||||
raise db.BadValueError('Property %s must be convertible '
|
||||
'to a Credentials instance (%s)' %
|
||||
(self.name, value))
|
||||
#if value is not None and not isinstance(value, Credentials):
|
||||
# return None
|
||||
return value
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
# TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
|
||||
# and subclass mechanics to use new_from_dict, to_dict,
|
||||
# from_dict, etc.
|
||||
class CredentialsNDBProperty(ndb.BlobProperty):
|
||||
"""App Engine NDB datastore Property for Credentials.
|
||||
|
||||
Serves the same purpose as the DB CredentialsProperty, but for NDB models.
|
||||
Since CredentialsProperty stores data as a blob and this inherits from
|
||||
BlobProperty, the data in the datastore will be the same as in the DB case.
|
||||
|
||||
Utility property that allows easy storage and retrieval of Credentials and
|
||||
subclasses.
|
||||
"""
|
||||
def _validate(self, value):
|
||||
"""Validates a value as a proper credentials object.
|
||||
|
||||
Args:
|
||||
value: A value to be set on the property.
|
||||
|
||||
Raises:
|
||||
TypeError if the value is not an instance of Credentials.
|
||||
"""
|
||||
logger.info('validate: Got type %s', type(value))
|
||||
if value is not None and not isinstance(value, Credentials):
|
||||
raise TypeError('Property %s must be convertible to a credentials '
|
||||
'instance; received: %s.' % (self._name, value))
|
||||
|
||||
def _to_base_type(self, value):
|
||||
"""Converts our validated value to a JSON serialized string.
|
||||
|
||||
Args:
|
||||
value: A value to be set in the datastore.
|
||||
|
||||
Returns:
|
||||
A JSON serialized version of the credential, else '' if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return ''
|
||||
else:
|
||||
return value.to_json()
|
||||
|
||||
def _from_base_type(self, value):
|
||||
"""Converts our stored JSON string back to the desired type.
|
||||
|
||||
Args:
|
||||
value: A value from the datastore to be converted to the desired type.
|
||||
|
||||
Returns:
|
||||
A deserialized Credentials (or subclass) object, else None if the
|
||||
value can't be parsed.
|
||||
"""
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
# Uses the from_json method of the implied class of value
|
||||
credentials = Credentials.new_from_json(value)
|
||||
except ValueError:
|
||||
credentials = None
|
||||
return credentials
|
||||
|
||||
|
||||
class StorageByKeyName(Storage):
|
||||
"""Store and retrieve a credential to and from the App Engine datastore.
|
||||
|
||||
This Storage helper presumes the Credentials have been stored as a
|
||||
CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
|
||||
that entities are stored by key_name.
|
||||
"""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, model, key_name, property_name, cache=None, user=None):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: db.Model or ndb.Model, model class
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
property_name: string, name of the property that is a CredentialsProperty
|
||||
or CredentialsNDBProperty.
|
||||
cache: memcache, a write-through cache to put in front of the datastore.
|
||||
If the model you are using is an NDB model, using a cache will be
|
||||
redundant since the model uses an instance cache and memcache for you.
|
||||
user: users.User object, optional. Can be used to grab user ID as a
|
||||
key_name if no key name is specified.
|
||||
"""
|
||||
if key_name is None:
|
||||
if user is None:
|
||||
raise ValueError('StorageByKeyName called with no key name or user.')
|
||||
key_name = user.user_id()
|
||||
|
||||
self._model = model
|
||||
self._key_name = key_name
|
||||
self._property_name = property_name
|
||||
self._cache = cache
|
||||
|
||||
def _is_ndb(self):
|
||||
"""Determine whether the model of the instance is an NDB model.
|
||||
|
||||
Returns:
|
||||
Boolean indicating whether or not the model is an NDB or DB model.
|
||||
"""
|
||||
# issubclass will fail if one of the arguments is not a class, only need
|
||||
# worry about new-style classes since ndb and db models are new-style
|
||||
if isinstance(self._model, type):
|
||||
if ndb is not None and issubclass(self._model, ndb.Model):
|
||||
return True
|
||||
elif issubclass(self._model, db.Model):
|
||||
return False
|
||||
|
||||
raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
|
||||
|
||||
def _get_entity(self):
|
||||
"""Retrieve entity from datastore.
|
||||
|
||||
Uses a different model method for db or ndb models.
|
||||
|
||||
Returns:
|
||||
Instance of the model corresponding to the current storage object
|
||||
and stored using the key name of the storage object.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
return self._model.get_by_id(self._key_name)
|
||||
else:
|
||||
return self._model.get_by_key_name(self._key_name)
|
||||
|
||||
def _delete_entity(self):
|
||||
"""Delete entity from datastore.
|
||||
|
||||
Attempts to delete using the key_name stored on the object, whether or not
|
||||
the given key is in the datastore.
|
||||
"""
|
||||
if self._is_ndb():
|
||||
ndb.Key(self._model, self._key_name).delete()
|
||||
else:
|
||||
entity_key = db.Key.from_path(self._model.kind(), self._key_name)
|
||||
db.delete(entity_key)
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
if self._cache:
|
||||
json = self._cache.get(self._key_name)
|
||||
if json:
|
||||
credentials = Credentials.new_from_json(json)
|
||||
if credentials is None:
|
||||
entity = self._get_entity()
|
||||
if entity is not None:
|
||||
credentials = getattr(entity, self._property_name)
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
if credentials and hasattr(credentials, 'set_store'):
|
||||
credentials.set_store(self)
|
||||
return credentials
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_put(self, credentials):
|
||||
"""Write a Credentials to the datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
entity = self._model.get_or_insert(self._key_name)
|
||||
setattr(entity, self._property_name, credentials)
|
||||
entity.put()
|
||||
if self._cache:
|
||||
self._cache.set(self._key_name, credentials.to_json())
|
||||
|
||||
@db.non_transactional(allow_existing=True)
|
||||
def locked_delete(self):
|
||||
"""Delete Credential from datastore."""
|
||||
|
||||
if self._cache:
|
||||
self._cache.delete(self._key_name)
|
||||
|
||||
self._delete_entity()
|
||||
|
||||
|
||||
class CredentialsModel(db.Model):
|
||||
"""Storage for OAuth 2.0 Credentials
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsProperty()
|
||||
|
||||
|
||||
if ndb is not None:
|
||||
class CredentialsNDBModel(ndb.Model):
|
||||
"""NDB Model for storage of OAuth 2.0 Credentials
|
||||
|
||||
Since this model uses the same kind as CredentialsModel and has a property
|
||||
which can serialize and deserialize Credentials correctly, it can be used
|
||||
interchangeably with a CredentialsModel to access, insert and delete the
|
||||
same entities. This simply provides an NDB model for interacting with the
|
||||
same data the DB model interacts with.
|
||||
|
||||
Storage of the model is keyed by the user.user_id().
|
||||
"""
|
||||
credentials = CredentialsNDBProperty()
|
||||
|
||||
@classmethod
|
||||
def _get_kind(cls):
|
||||
"""Return the kind name for this class."""
|
||||
return 'CredentialsModel'
|
||||
|
||||
|
||||
def _build_state_value(request_handler, user):
|
||||
"""Composes the value for the 'state' parameter.
|
||||
|
||||
Packs the current request URI and an XSRF token into an opaque string that
|
||||
can be passed to the authentication server via the 'state' parameter.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, The request.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Returns:
|
||||
The state value as a string.
|
||||
"""
|
||||
uri = request_handler.request.url
|
||||
token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
|
||||
action_id=str(uri))
|
||||
return uri + ':' + token
|
||||
|
||||
|
||||
def _parse_state_value(state, user):
|
||||
"""Parse the value of the 'state' parameter.
|
||||
|
||||
Parses the value and validates the XSRF token in the state parameter.
|
||||
|
||||
Args:
|
||||
state: string, The value of the state parameter.
|
||||
user: google.appengine.api.users.User, The current user.
|
||||
|
||||
Raises:
|
||||
InvalidXsrfTokenError: if the XSRF token is invalid.
|
||||
|
||||
Returns:
|
||||
The redirect URI.
|
||||
"""
|
||||
uri, token = state.rsplit(':', 1)
|
||||
if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
|
||||
action_id=uri):
|
||||
raise InvalidXsrfTokenError()
|
||||
|
||||
return uri
|
||||
|
||||
|
||||
class OAuth2Decorator(object):
|
||||
"""Utility for making OAuth 2.0 easier.
|
||||
|
||||
Instantiate and then use with oauth_required or oauth_aware
|
||||
as decorators on webapp.RequestHandler methods.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2Decorator(
|
||||
client_id='837...ent.com',
|
||||
client_secret='Qh...wwI',
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be used
|
||||
# in API calls
|
||||
|
||||
"""
|
||||
|
||||
def set_credentials(self, credentials):
|
||||
self._tls.credentials = credentials
|
||||
|
||||
def get_credentials(self):
|
||||
"""A thread local Credentials object.
|
||||
|
||||
Returns:
|
||||
A client.Credentials object, or None if credentials hasn't been set in
|
||||
this thread yet, which may happen when calling has_credentials inside
|
||||
oauth_aware.
|
||||
"""
|
||||
return getattr(self._tls, 'credentials', None)
|
||||
|
||||
credentials = property(get_credentials, set_credentials)
|
||||
|
||||
def set_flow(self, flow):
|
||||
self._tls.flow = flow
|
||||
|
||||
def get_flow(self):
|
||||
"""A thread local Flow object.
|
||||
|
||||
Returns:
|
||||
A credentials.Flow object, or None if the flow hasn't been set in this
|
||||
thread yet, which happens in _create_flow() since Flows are created
|
||||
lazily.
|
||||
"""
|
||||
return getattr(self._tls, 'flow', None)
|
||||
|
||||
flow = property(get_flow, set_flow)
|
||||
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, client_id, client_secret, scope,
|
||||
auth_uri=GOOGLE_AUTH_URI,
|
||||
token_uri=GOOGLE_TOKEN_URI,
|
||||
revoke_uri=GOOGLE_REVOKE_URI,
|
||||
user_agent=None,
|
||||
message=None,
|
||||
callback_path='/oauth2callback',
|
||||
token_response_param=None,
|
||||
_storage_class=StorageByKeyName,
|
||||
_credentials_class=CredentialsModel,
|
||||
_credentials_property_name='credentials',
|
||||
**kwargs):
|
||||
|
||||
"""Constructor for OAuth2Decorator
|
||||
|
||||
Args:
|
||||
client_id: string, client identifier.
|
||||
client_secret: string client secret.
|
||||
scope: string or iterable of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
auth_uri: string, URI for authorization endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
token_uri: string, URI for token endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
revoke_uri: string, URI for revoke endpoint. For convenience
|
||||
defaults to Google's endpoints but any OAuth 2.0 provider can be used.
|
||||
user_agent: string, User agent of your application, default to None.
|
||||
message: Message to display if there are problems with the OAuth 2.0
|
||||
configuration. The message may contain HTML and will be presented on the
|
||||
web interface for any method that uses the decorator.
|
||||
callback_path: string, The absolute path to use as the callback URI. Note
|
||||
that this must match up with the URI given when registering the
|
||||
application in the APIs Console.
|
||||
token_response_param: string. If provided, the full JSON response
|
||||
to the access token request will be encoded and included in this query
|
||||
parameter in the callback URI. This is useful with providers (e.g.
|
||||
wordpress.com) that include extra fields that the client may want.
|
||||
_storage_class: "Protected" keyword argument not typically provided to
|
||||
this constructor. A storage class to aid in storing a Credentials object
|
||||
for a user in the datastore. Defaults to StorageByKeyName.
|
||||
_credentials_class: "Protected" keyword argument not typically provided to
|
||||
this constructor. A db or ndb Model class to hold credentials. Defaults
|
||||
to CredentialsModel.
|
||||
_credentials_property_name: "Protected" keyword argument not typically
|
||||
provided to this constructor. A string indicating the name of the field
|
||||
on the _credentials_class where a Credentials object will be stored.
|
||||
Defaults to 'credentials'.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
|
||||
"""
|
||||
self._tls = threading.local()
|
||||
self.flow = None
|
||||
self.credentials = None
|
||||
self._client_id = client_id
|
||||
self._client_secret = client_secret
|
||||
self._scope = util.scopes_to_string(scope)
|
||||
self._auth_uri = auth_uri
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._user_agent = user_agent
|
||||
self._kwargs = kwargs
|
||||
self._message = message
|
||||
self._in_error = False
|
||||
self._callback_path = callback_path
|
||||
self._token_response_param = token_response_param
|
||||
self._storage_class = _storage_class
|
||||
self._credentials_class = _credentials_class
|
||||
self._credentials_property_name = _credentials_property_name
|
||||
|
||||
def _display_error_message(self, request_handler):
|
||||
request_handler.response.out.write('<html><body>')
|
||||
request_handler.response.out.write(_safe_html(self._message))
|
||||
request_handler.response.out.write('</body></html>')
|
||||
|
||||
def oauth_required(self, method):
|
||||
"""Decorator that starts the OAuth 2.0 dance.
|
||||
|
||||
Starts the OAuth dance for the logged in user if they haven't already
|
||||
granted access for this application.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def check_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
# Store the request URI in 'state' so we can use it later
|
||||
self.flow.params['state'] = _build_state_value(request_handler, user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
|
||||
if not self.has_credentials():
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
except AccessTokenRefreshError:
|
||||
return request_handler.redirect(self.authorize_url())
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
|
||||
return check_oauth
|
||||
|
||||
def _create_flow(self, request_handler):
|
||||
"""Create the Flow object.
|
||||
|
||||
The Flow is calculated lazily since we don't know where this app is
|
||||
running until it receives a request, at which point redirect_uri can be
|
||||
calculated and then the Flow object can be constructed.
|
||||
|
||||
Args:
|
||||
request_handler: webapp.RequestHandler, the request handler.
|
||||
"""
|
||||
if self.flow is None:
|
||||
redirect_uri = request_handler.request.relative_url(
|
||||
self._callback_path) # Usually /oauth2callback
|
||||
self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
|
||||
self._scope, redirect_uri=redirect_uri,
|
||||
user_agent=self._user_agent,
|
||||
auth_uri=self._auth_uri,
|
||||
token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri,
|
||||
**self._kwargs)
|
||||
|
||||
def oauth_aware(self, method):
|
||||
"""Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
|
||||
|
||||
Does all the setup for the OAuth dance, but doesn't initiate it.
|
||||
This decorator is useful if you want to create a page that knows
|
||||
whether or not the user has granted access to this application.
|
||||
From within a method decorated with @oauth_aware the has_credentials()
|
||||
and authorize_url() methods can be called.
|
||||
|
||||
Args:
|
||||
method: callable, to be decorated method of a webapp.RequestHandler
|
||||
instance.
|
||||
"""
|
||||
|
||||
def setup_oauth(request_handler, *args, **kwargs):
|
||||
if self._in_error:
|
||||
self._display_error_message(request_handler)
|
||||
return
|
||||
|
||||
user = users.get_current_user()
|
||||
# Don't use @login_decorator as this could be used in a POST request.
|
||||
if not user:
|
||||
request_handler.redirect(users.create_login_url(
|
||||
request_handler.request.uri))
|
||||
return
|
||||
|
||||
self._create_flow(request_handler)
|
||||
|
||||
self.flow.params['state'] = _build_state_value(request_handler, user)
|
||||
self.credentials = self._storage_class(
|
||||
self._credentials_class, None,
|
||||
self._credentials_property_name, user=user).get()
|
||||
try:
|
||||
resp = method(request_handler, *args, **kwargs)
|
||||
finally:
|
||||
self.credentials = None
|
||||
return resp
|
||||
return setup_oauth
|
||||
|
||||
|
||||
def has_credentials(self):
|
||||
"""True if for the logged in user there are valid access Credentials.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
return self.credentials is not None and not self.credentials.invalid
|
||||
|
||||
def authorize_url(self):
|
||||
"""Returns the URL to start the OAuth dance.
|
||||
|
||||
Must only be called from with a webapp.RequestHandler subclassed method
|
||||
that had been decorated with either @oauth_required or @oauth_aware.
|
||||
"""
|
||||
url = self.flow.step1_get_authorize_url()
|
||||
return str(url)
|
||||
|
||||
def http(self, *args, **kwargs):
|
||||
"""Returns an authorized http instance.
|
||||
|
||||
Must only be called from within an @oauth_required decorated method, or
|
||||
from within an @oauth_aware decorated method where has_credentials()
|
||||
returns True.
|
||||
|
||||
Args:
|
||||
*args: Positional arguments passed to httplib2.Http constructor.
|
||||
**kwargs: Positional arguments passed to httplib2.Http constructor.
|
||||
"""
|
||||
return self.credentials.authorize(httplib2.Http(*args, **kwargs))
|
||||
|
||||
@property
|
||||
def callback_path(self):
|
||||
"""The absolute path where the callback will occur.
|
||||
|
||||
Note this is the absolute path, not the absolute URI, that will be
|
||||
calculated by the decorator at runtime. See callback_handler() for how this
|
||||
should be used.
|
||||
|
||||
Returns:
|
||||
The callback path as a string.
|
||||
"""
|
||||
return self._callback_path
|
||||
|
||||
|
||||
def callback_handler(self):
|
||||
"""RequestHandler for the OAuth 2.0 redirect callback.
|
||||
|
||||
Usage::
|
||||
|
||||
app = webapp.WSGIApplication([
|
||||
('/index', MyIndexHandler),
|
||||
...,
|
||||
(decorator.callback_path, decorator.callback_handler())
|
||||
])
|
||||
|
||||
Returns:
|
||||
A webapp.RequestHandler that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
decorator = self
|
||||
|
||||
class OAuth2Handler(webapp.RequestHandler):
|
||||
"""Handler for the redirect_uri of the OAuth 2.0 dance."""
|
||||
|
||||
@login_required
|
||||
def get(self):
|
||||
error = self.request.get('error')
|
||||
if error:
|
||||
errormsg = self.request.get('error_description', error)
|
||||
self.response.out.write(
|
||||
'The authorization request failed: %s' % _safe_html(errormsg))
|
||||
else:
|
||||
user = users.get_current_user()
|
||||
decorator._create_flow(self)
|
||||
credentials = decorator.flow.step2_exchange(self.request.params)
|
||||
decorator._storage_class(
|
||||
decorator._credentials_class, None,
|
||||
decorator._credentials_property_name, user=user).put(credentials)
|
||||
redirect_uri = _parse_state_value(str(self.request.get('state')),
|
||||
user)
|
||||
|
||||
if decorator._token_response_param and credentials.token_response:
|
||||
resp_json = json.dumps(credentials.token_response)
|
||||
redirect_uri = util._add_query_parameter(
|
||||
redirect_uri, decorator._token_response_param, resp_json)
|
||||
|
||||
self.redirect(redirect_uri)
|
||||
|
||||
return OAuth2Handler
|
||||
|
||||
def callback_application(self):
|
||||
"""WSGI application for handling the OAuth 2.0 redirect callback.
|
||||
|
||||
If you need finer grained control use `callback_handler` which returns just
|
||||
the webapp.RequestHandler.
|
||||
|
||||
Returns:
|
||||
A webapp.WSGIApplication that handles the redirect back from the
|
||||
server during the OAuth 2.0 dance.
|
||||
"""
|
||||
return webapp.WSGIApplication([
|
||||
(self.callback_path, self.callback_handler())
|
||||
])
|
||||
|
||||
|
||||
class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
|
||||
"""An OAuth2Decorator that builds from a clientsecrets file.
|
||||
|
||||
Uses a clientsecrets file as the source for all the information when
|
||||
constructing an OAuth2Decorator.
|
||||
|
||||
::
|
||||
|
||||
decorator = OAuth2DecoratorFromClientSecrets(
|
||||
os.path.join(os.path.dirname(__file__), 'client_secrets.json')
|
||||
scope='https://www.googleapis.com/auth/plus')
|
||||
|
||||
class MainHandler(webapp.RequestHandler):
|
||||
@decorator.oauth_required
|
||||
def get(self):
|
||||
http = decorator.http()
|
||||
# http is authorized with the user's Credentials and can be used
|
||||
# in API calls
|
||||
|
||||
"""
|
||||
|
||||
@util.positional(3)
|
||||
def __init__(self, filename, scope, message=None, cache=None, **kwargs):
|
||||
"""Constructor
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or iterable of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may contain HTML
|
||||
and will be presented on the web interface for any method that uses the
|
||||
decorator.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
**kwargs: dict, Keyword arguments are passed along as kwargs to
|
||||
the OAuth2WebServerFlow constructor.
|
||||
"""
|
||||
client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
|
||||
if client_type not in [
|
||||
clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
|
||||
raise InvalidClientSecretsError(
|
||||
"OAuth2Decorator doesn't support this OAuth 2.0 flow.")
|
||||
constructor_kwargs = dict(kwargs)
|
||||
constructor_kwargs.update({
|
||||
'auth_uri': client_info['auth_uri'],
|
||||
'token_uri': client_info['token_uri'],
|
||||
'message': message,
|
||||
})
|
||||
revoke_uri = client_info.get('revoke_uri')
|
||||
if revoke_uri is not None:
|
||||
constructor_kwargs['revoke_uri'] = revoke_uri
|
||||
super(OAuth2DecoratorFromClientSecrets, self).__init__(
|
||||
client_info['client_id'], client_info['client_secret'],
|
||||
scope, **constructor_kwargs)
|
||||
if message is not None:
|
||||
self._message = message
|
||||
else:
|
||||
self._message = 'Please configure your application for OAuth 2.0.'
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def oauth2decorator_from_clientsecrets(filename, scope,
|
||||
message=None, cache=None):
|
||||
"""Creates an OAuth2Decorator populated from a clientsecrets file.
|
||||
|
||||
Args:
|
||||
filename: string, File name of client secrets.
|
||||
scope: string or list of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
message: string, A friendly string to display to the user if the
|
||||
clientsecrets file is missing or invalid. The message may contain HTML and
|
||||
will be presented on the web interface for any method that uses the
|
||||
decorator.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. See clientsecrets.loadfile() for details.
|
||||
|
||||
Returns: An OAuth2Decorator
|
||||
|
||||
"""
|
||||
return OAuth2DecoratorFromClientSecrets(filename, scope,
|
||||
message=message, cache=cache)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,163 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for reading OAuth 2.0 client secret files.
|
||||
|
||||
A client_secrets.json file contains all the information needed to interact with
|
||||
an OAuth 2.0 protected service.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
import six
|
||||
|
||||
|
||||
# Properties that make a client_secrets.json file valid.
|
||||
TYPE_WEB = 'web'
|
||||
TYPE_INSTALLED = 'installed'
|
||||
|
||||
VALID_CLIENT = {
|
||||
TYPE_WEB: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
TYPE_INSTALLED: {
|
||||
'required': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'auth_uri',
|
||||
'token_uri',
|
||||
],
|
||||
'string': [
|
||||
'client_id',
|
||||
'client_secret',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidClientSecretsError(Error):
|
||||
"""Format of ClientSecrets file is invalid."""
|
||||
pass
|
||||
|
||||
|
||||
def _validate_clientsecrets(obj):
|
||||
_INVALID_FILE_FORMAT_MSG = (
|
||||
'Invalid file format. See '
|
||||
'https://developers.google.com/api-client-library/'
|
||||
'python/guide/aaa_client_secrets')
|
||||
|
||||
if obj is None:
|
||||
raise InvalidClientSecretsError(_INVALID_FILE_FORMAT_MSG)
|
||||
if len(obj) != 1:
|
||||
raise InvalidClientSecretsError(
|
||||
_INVALID_FILE_FORMAT_MSG + ' '
|
||||
'Expected a JSON object with a single property for a "web" or '
|
||||
'"installed" application')
|
||||
client_type = tuple(obj)[0]
|
||||
if client_type not in VALID_CLIENT:
|
||||
raise InvalidClientSecretsError('Unknown client type: %s.' % (client_type,))
|
||||
client_info = obj[client_type]
|
||||
for prop_name in VALID_CLIENT[client_type]['required']:
|
||||
if prop_name not in client_info:
|
||||
raise InvalidClientSecretsError(
|
||||
'Missing property "%s" in a client type of "%s".' % (prop_name,
|
||||
client_type))
|
||||
for prop_name in VALID_CLIENT[client_type]['string']:
|
||||
if client_info[prop_name].startswith('[['):
|
||||
raise InvalidClientSecretsError(
|
||||
'Property "%s" is not configured.' % prop_name)
|
||||
return client_type, client_info
|
||||
|
||||
|
||||
def load(fp):
|
||||
obj = json.load(fp)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loads(s):
|
||||
obj = json.loads(s)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def _loadfile(filename):
|
||||
try:
|
||||
with open(filename, 'r') as fp:
|
||||
obj = json.load(fp)
|
||||
except IOError:
|
||||
raise InvalidClientSecretsError('File not found: "%s"' % filename)
|
||||
return _validate_clientsecrets(obj)
|
||||
|
||||
|
||||
def loadfile(filename, cache=None):
|
||||
"""Loading of client_secrets JSON file, optionally backed by a cache.
|
||||
|
||||
Typical cache storage would be App Engine memcache service,
|
||||
but you can pass in any other cache client that implements
|
||||
these methods:
|
||||
|
||||
* ``get(key, namespace=ns)``
|
||||
* ``set(key, value, namespace=ns)``
|
||||
|
||||
Usage::
|
||||
|
||||
# without caching
|
||||
client_type, client_info = loadfile('secrets.json')
|
||||
# using App Engine memcache service
|
||||
from google.appengine.api import memcache
|
||||
client_type, client_info = loadfile('secrets.json', cache=memcache)
|
||||
|
||||
Args:
|
||||
filename: string, Path to a client_secrets.json file on a filesystem.
|
||||
cache: An optional cache service client that implements get() and set()
|
||||
methods. If not specified, the file is always being loaded from
|
||||
a filesystem.
|
||||
|
||||
Raises:
|
||||
InvalidClientSecretsError: In case of a validation error or some
|
||||
I/O failure. Can happen only on cache miss.
|
||||
|
||||
Returns:
|
||||
(client_type, client_info) tuple, as _loadfile() normally would.
|
||||
JSON contents is validated only during first load. Cache hits are not
|
||||
validated.
|
||||
"""
|
||||
_SECRET_NAMESPACE = 'oauth2client:secrets#ns'
|
||||
|
||||
if not cache:
|
||||
return _loadfile(filename)
|
||||
|
||||
obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
|
||||
if obj is None:
|
||||
client_type, client_info = _loadfile(filename)
|
||||
obj = {client_type: client_info}
|
||||
cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
|
||||
|
||||
return next(six.iteritems(obj))
|
||||
@@ -1,429 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
"""Crypto-related routines for oauth2client."""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import six
|
||||
|
||||
|
||||
CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
|
||||
AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
|
||||
MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AppIdentityError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
from OpenSSL import crypto
|
||||
|
||||
class OpenSSLVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey, OpenSSL.crypto.PKey, The public key to verify with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string, The message to verify.
|
||||
signature: string, The signature on the message.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the public
|
||||
key that this object was constructed with.
|
||||
"""
|
||||
try:
|
||||
if isinstance(message, six.text_type):
|
||||
message = message.encode('utf-8')
|
||||
crypto.verify(self._pubkey, signature, message, 'sha256')
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
|
||||
expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key_pem can't be parsed.
|
||||
"""
|
||||
if is_x509_cert:
|
||||
pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
|
||||
else:
|
||||
pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
|
||||
return OpenSSLVerifier(pubkey)
|
||||
|
||||
|
||||
class OpenSSLSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: bytes, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
if isinstance(message, six.text_type):
|
||||
message = message.encode('utf-8')
|
||||
return crypto.sign(self._key, message, 'sha256')
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password=b'notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PKCS12 or PEM format.
|
||||
password: string, password for the private key file.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
OpenSSL.crypto.Error if the key can't be parsed.
|
||||
"""
|
||||
parsed_pem_key = _parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, parsed_pem_key)
|
||||
else:
|
||||
if isinstance(password, six.text_type):
|
||||
password = password.encode('utf-8')
|
||||
pkey = crypto.load_pkcs12(key, password).get_privatekey()
|
||||
return OpenSSLSigner(pkey)
|
||||
|
||||
|
||||
def pkcs12_key_as_pem(private_key_text, private_key_password):
|
||||
"""Convert the contents of a PKCS12 key to PEM using OpenSSL.
|
||||
|
||||
Args:
|
||||
private_key_text: String. Private key.
|
||||
private_key_password: String. Password for PKCS12.
|
||||
|
||||
Returns:
|
||||
String. PEM contents of ``private_key_text``.
|
||||
"""
|
||||
decoded_body = base64.b64decode(private_key_text)
|
||||
if isinstance(private_key_password, six.string_types):
|
||||
private_key_password = private_key_password.encode('ascii')
|
||||
|
||||
pkcs12 = crypto.load_pkcs12(decoded_body, private_key_password)
|
||||
return crypto.dump_privatekey(crypto.FILETYPE_PEM,
|
||||
pkcs12.get_privatekey())
|
||||
except ImportError:
|
||||
OpenSSLVerifier = None
|
||||
OpenSSLSigner = None
|
||||
def pkcs12_key_as_pem(*args, **kwargs):
|
||||
raise NotImplementedError('pkcs12_key_as_pem requires OpenSSL.')
|
||||
|
||||
|
||||
try:
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Hash import SHA256
|
||||
from Crypto.Signature import PKCS1_v1_5
|
||||
from Crypto.Util.asn1 import DerSequence
|
||||
|
||||
|
||||
class PyCryptoVerifier(object):
|
||||
"""Verifies the signature on a message."""
|
||||
|
||||
def __init__(self, pubkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
|
||||
"""
|
||||
self._pubkey = pubkey
|
||||
|
||||
def verify(self, message, signature):
|
||||
"""Verifies a message against a signature.
|
||||
|
||||
Args:
|
||||
message: string, The message to verify.
|
||||
signature: string, The signature on the message.
|
||||
|
||||
Returns:
|
||||
True if message was signed by the private key associated with the public
|
||||
key that this object was constructed with.
|
||||
"""
|
||||
try:
|
||||
return PKCS1_v1_5.new(self._pubkey).verify(
|
||||
SHA256.new(message), signature)
|
||||
except:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def from_string(key_pem, is_x509_cert):
|
||||
"""Construct a Verified instance from a string.
|
||||
|
||||
Args:
|
||||
key_pem: string, public key in PEM format.
|
||||
is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
|
||||
expected to be an RSA key in PEM format.
|
||||
|
||||
Returns:
|
||||
Verifier instance.
|
||||
"""
|
||||
if is_x509_cert:
|
||||
if isinstance(key_pem, six.text_type):
|
||||
key_pem = key_pem.encode('ascii')
|
||||
pemLines = key_pem.replace(b' ', b'').split()
|
||||
certDer = _urlsafe_b64decode(b''.join(pemLines[1:-1]))
|
||||
certSeq = DerSequence()
|
||||
certSeq.decode(certDer)
|
||||
tbsSeq = DerSequence()
|
||||
tbsSeq.decode(certSeq[0])
|
||||
pubkey = RSA.importKey(tbsSeq[6])
|
||||
else:
|
||||
pubkey = RSA.importKey(key_pem)
|
||||
return PyCryptoVerifier(pubkey)
|
||||
|
||||
|
||||
class PyCryptoSigner(object):
|
||||
"""Signs messages with a private key."""
|
||||
|
||||
def __init__(self, pkey):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
|
||||
"""
|
||||
self._key = pkey
|
||||
|
||||
def sign(self, message):
|
||||
"""Signs a message.
|
||||
|
||||
Args:
|
||||
message: string, Message to be signed.
|
||||
|
||||
Returns:
|
||||
string, The signature of the message for the given key.
|
||||
"""
|
||||
if isinstance(message, six.text_type):
|
||||
message = message.encode('utf-8')
|
||||
return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
|
||||
|
||||
@staticmethod
|
||||
def from_string(key, password='notasecret'):
|
||||
"""Construct a Signer instance from a string.
|
||||
|
||||
Args:
|
||||
key: string, private key in PEM format.
|
||||
password: string, password for private key file. Unused for PEM files.
|
||||
|
||||
Returns:
|
||||
Signer instance.
|
||||
|
||||
Raises:
|
||||
NotImplementedError if they key isn't in PEM format.
|
||||
"""
|
||||
parsed_pem_key = _parse_pem_key(key)
|
||||
if parsed_pem_key:
|
||||
pkey = RSA.importKey(parsed_pem_key)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
'PKCS12 format is not supported by the PyCrypto library. '
|
||||
'Try converting to a "PEM" '
|
||||
'(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
|
||||
'or using PyOpenSSL if native code is an option.')
|
||||
return PyCryptoSigner(pkey)
|
||||
|
||||
except ImportError:
|
||||
PyCryptoVerifier = None
|
||||
PyCryptoSigner = None
|
||||
|
||||
|
||||
if OpenSSLSigner:
|
||||
Signer = OpenSSLSigner
|
||||
Verifier = OpenSSLVerifier
|
||||
elif PyCryptoSigner:
|
||||
Signer = PyCryptoSigner
|
||||
Verifier = PyCryptoVerifier
|
||||
else:
|
||||
raise ImportError('No encryption library found. Please install either '
|
||||
'PyOpenSSL, or PyCrypto 2.6 or later')
|
||||
|
||||
|
||||
def _parse_pem_key(raw_key_input):
|
||||
"""Identify and extract PEM keys.
|
||||
|
||||
Determines whether the given key is in the format of PEM key, and extracts
|
||||
the relevant part of the key if it is.
|
||||
|
||||
Args:
|
||||
raw_key_input: The contents of a private key file (either PEM or PKCS12).
|
||||
|
||||
Returns:
|
||||
string, The actual key if the contents are from a PEM file, or else None.
|
||||
"""
|
||||
offset = raw_key_input.find(b'-----BEGIN ')
|
||||
if offset != -1:
|
||||
return raw_key_input[offset:]
|
||||
|
||||
|
||||
def _urlsafe_b64encode(raw_bytes):
|
||||
if isinstance(raw_bytes, six.text_type):
|
||||
raw_bytes = raw_bytes.encode('utf-8')
|
||||
return base64.urlsafe_b64encode(raw_bytes).decode('ascii').rstrip('=')
|
||||
|
||||
|
||||
def _urlsafe_b64decode(b64string):
|
||||
# Guard against unicode strings, which base64 can't handle.
|
||||
if isinstance(b64string, six.text_type):
|
||||
b64string = b64string.encode('ascii')
|
||||
padded = b64string + b'=' * (4 - len(b64string) % 4)
|
||||
return base64.urlsafe_b64decode(padded)
|
||||
|
||||
|
||||
def _json_encode(data):
|
||||
return json.dumps(data, separators=(',', ':'))
|
||||
|
||||
|
||||
def make_signed_jwt(signer, payload):
|
||||
"""Make a signed JWT.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
signer: crypt.Signer, Cryptographic signer.
|
||||
payload: dict, Dictionary of data to convert to JSON and then sign.
|
||||
|
||||
Returns:
|
||||
string, The JWT for the payload.
|
||||
"""
|
||||
header = {'typ': 'JWT', 'alg': 'RS256'}
|
||||
|
||||
segments = [
|
||||
_urlsafe_b64encode(_json_encode(header)),
|
||||
_urlsafe_b64encode(_json_encode(payload)),
|
||||
]
|
||||
signing_input = '.'.join(segments)
|
||||
|
||||
signature = signer.sign(signing_input)
|
||||
segments.append(_urlsafe_b64encode(signature))
|
||||
|
||||
logger.debug(str(segments))
|
||||
|
||||
return '.'.join(segments)
|
||||
|
||||
|
||||
def verify_signed_jwt_with_certs(jwt, certs, audience):
|
||||
"""Verify a JWT against public certs.
|
||||
|
||||
See http://self-issued.info/docs/draft-jones-json-web-token.html.
|
||||
|
||||
Args:
|
||||
jwt: string, A JWT.
|
||||
certs: dict, Dictionary where values of public keys in PEM format.
|
||||
audience: string, The audience, 'aud', that this JWT should contain. If
|
||||
None then the JWT's 'aud' parameter is not verified.
|
||||
|
||||
Returns:
|
||||
dict, The deserialized JSON payload in the JWT.
|
||||
|
||||
Raises:
|
||||
AppIdentityError if any checks are failed.
|
||||
"""
|
||||
segments = jwt.split('.')
|
||||
|
||||
if len(segments) != 3:
|
||||
raise AppIdentityError('Wrong number of segments in token: %s' % jwt)
|
||||
signed = '%s.%s' % (segments[0], segments[1])
|
||||
|
||||
signature = _urlsafe_b64decode(segments[2])
|
||||
|
||||
# Parse token.
|
||||
json_body = _urlsafe_b64decode(segments[1])
|
||||
try:
|
||||
parsed = json.loads(json_body.decode('utf-8'))
|
||||
except:
|
||||
raise AppIdentityError('Can\'t parse token: %s' % json_body)
|
||||
|
||||
# Check signature.
|
||||
verified = False
|
||||
for pem in certs.values():
|
||||
verifier = Verifier.from_string(pem, True)
|
||||
if verifier.verify(signed, signature):
|
||||
verified = True
|
||||
break
|
||||
if not verified:
|
||||
raise AppIdentityError('Invalid token signature: %s' % jwt)
|
||||
|
||||
# Check creation timestamp.
|
||||
iat = parsed.get('iat')
|
||||
if iat is None:
|
||||
raise AppIdentityError('No iat field in token: %s' % json_body)
|
||||
earliest = iat - CLOCK_SKEW_SECS
|
||||
|
||||
# Check expiration timestamp.
|
||||
now = int(time.time())
|
||||
exp = parsed.get('exp')
|
||||
if exp is None:
|
||||
raise AppIdentityError('No exp field in token: %s' % json_body)
|
||||
if exp >= now + MAX_TOKEN_LIFETIME_SECS:
|
||||
raise AppIdentityError('exp field too far in future: %s' % json_body)
|
||||
latest = exp + CLOCK_SKEW_SECS
|
||||
|
||||
if now < earliest:
|
||||
raise AppIdentityError('Token used too early, %d < %d: %s' %
|
||||
(now, earliest, json_body))
|
||||
if now > latest:
|
||||
raise AppIdentityError('Token used too late, %d > %d: %s' %
|
||||
(now, latest, json_body))
|
||||
|
||||
# Check audience.
|
||||
if audience is not None:
|
||||
aud = parsed.get('aud')
|
||||
if aud is None:
|
||||
raise AppIdentityError('No aud field in token: %s' % json_body)
|
||||
if aud != audience:
|
||||
raise AppIdentityError('Wrong recipient, %s != %s: %s' %
|
||||
(aud, audience, json_body))
|
||||
|
||||
return parsed
|
||||
@@ -1,136 +0,0 @@
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 utitilies for Google Developer Shell environment."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from oauth2client import client
|
||||
|
||||
|
||||
DEVSHELL_ENV = 'DEVSHELL_CLIENT_PORT'
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Errors for this module."""
|
||||
pass
|
||||
|
||||
|
||||
class CommunicationError(Error):
|
||||
"""Errors for communication with the Developer Shell server."""
|
||||
|
||||
|
||||
class NoDevshellServer(Error):
|
||||
"""Error when no Developer Shell server can be contacted."""
|
||||
|
||||
|
||||
# The request for credential information to the Developer Shell client socket is
|
||||
# always an empty PBLite-formatted JSON object, so just define it as a constant.
|
||||
CREDENTIAL_INFO_REQUEST_JSON = '[]'
|
||||
|
||||
|
||||
class CredentialInfoResponse(object):
|
||||
"""Credential information response from Developer Shell server.
|
||||
|
||||
The credential information response from Developer Shell socket is a
|
||||
PBLite-formatted JSON array with fields encoded by their index in the array:
|
||||
* Index 0 - user email
|
||||
* Index 1 - default project ID. None if the project context is not known.
|
||||
* Index 2 - OAuth2 access token. None if there is no valid auth context.
|
||||
"""
|
||||
|
||||
def __init__(self, json_string):
|
||||
"""Initialize the response data from JSON PBLite array."""
|
||||
pbl = json.loads(json_string)
|
||||
if not isinstance(pbl, list):
|
||||
raise ValueError('Not a list: ' + str(pbl))
|
||||
pbl_len = len(pbl)
|
||||
self.user_email = pbl[0] if pbl_len > 0 else None
|
||||
self.project_id = pbl[1] if pbl_len > 1 else None
|
||||
self.access_token = pbl[2] if pbl_len > 2 else None
|
||||
|
||||
|
||||
def _SendRecv():
|
||||
"""Communicate with the Developer Shell server socket."""
|
||||
|
||||
port = int(os.getenv(DEVSHELL_ENV, 0))
|
||||
if port == 0:
|
||||
raise NoDevshellServer()
|
||||
|
||||
import socket
|
||||
|
||||
sock = socket.socket()
|
||||
sock.connect(('localhost', port))
|
||||
|
||||
data = CREDENTIAL_INFO_REQUEST_JSON
|
||||
msg = '%s\n%s' % (len(data), data)
|
||||
sock.sendall(msg.encode())
|
||||
|
||||
header = sock.recv(6).decode()
|
||||
if '\n' not in header:
|
||||
raise CommunicationError('saw no newline in the first 6 bytes')
|
||||
len_str, json_str = header.split('\n', 1)
|
||||
to_read = int(len_str) - len(json_str)
|
||||
if to_read > 0:
|
||||
json_str += sock.recv(to_read, socket.MSG_WAITALL).decode()
|
||||
|
||||
return CredentialInfoResponse(json_str)
|
||||
|
||||
|
||||
class DevshellCredentials(client.GoogleCredentials):
|
||||
"""Credentials object for Google Developer Shell environment.
|
||||
|
||||
This object will allow a Google Developer Shell session to identify its user
|
||||
to Google and other OAuth 2.0 servers that can verify assertions. It can be
|
||||
used for the purpose of accessing data stored under the user account.
|
||||
|
||||
This credential does not require a flow to instantiate because it represents
|
||||
a two legged flow, and therefore has all of the required information to
|
||||
generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
def __init__(self, user_agent=None):
|
||||
super(DevshellCredentials, self).__init__(
|
||||
None, # access_token, initialized below
|
||||
None, # client_id
|
||||
None, # client_secret
|
||||
None, # refresh_token
|
||||
None, # token_expiry
|
||||
None, # token_uri
|
||||
user_agent)
|
||||
self._refresh(None)
|
||||
|
||||
def _refresh(self, http_request):
|
||||
self.devshell_response = _SendRecv()
|
||||
self.access_token = self.devshell_response.access_token
|
||||
|
||||
@property
|
||||
def user_email(self):
|
||||
return self.devshell_response.user_email
|
||||
|
||||
@property
|
||||
def project_id(self):
|
||||
return self.devshell_response.project_id
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
raise NotImplementedError(
|
||||
'Cannot load Developer Shell credentials from JSON.')
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize Developer Shell credentials.')
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""OAuth 2.0 utilities for Django.
|
||||
|
||||
Utilities for using OAuth 2.0 in conjunction with
|
||||
the Django datastore.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import oauth2client
|
||||
import base64
|
||||
import pickle
|
||||
|
||||
from django.db import models
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
|
||||
class CredentialsField(models.Field):
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
kwargs['null'] = True
|
||||
super(CredentialsField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "TextField"
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, oauth2client.client.Credentials):
|
||||
return value
|
||||
return pickle.loads(base64.b64decode(value))
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if value is None:
|
||||
return None
|
||||
return base64.b64encode(pickle.dumps(value))
|
||||
|
||||
|
||||
class FlowField(models.Field):
|
||||
|
||||
__metaclass__ = models.SubfieldBase
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'null' not in kwargs:
|
||||
kwargs['null'] = True
|
||||
super(FlowField, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "TextField"
|
||||
|
||||
def to_python(self, value):
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, oauth2client.client.Flow):
|
||||
return value
|
||||
return pickle.loads(base64.b64decode(value))
|
||||
|
||||
def get_db_prep_value(self, value, connection, prepared=False):
|
||||
if value is None:
|
||||
return None
|
||||
return base64.b64encode(pickle.dumps(value))
|
||||
|
||||
|
||||
class Storage(BaseStorage):
|
||||
"""Store and retrieve a single credential to and from
|
||||
the datastore.
|
||||
|
||||
This Storage helper presumes the Credentials
|
||||
have been stored as a CredenialsField
|
||||
on a db model class.
|
||||
"""
|
||||
|
||||
def __init__(self, model_class, key_name, key_value, property_name):
|
||||
"""Constructor for Storage.
|
||||
|
||||
Args:
|
||||
model: db.Model, model class
|
||||
key_name: string, key name for the entity that has the credentials
|
||||
key_value: string, key value for the entity that has the credentials
|
||||
property_name: string, name of the property that is an CredentialsProperty
|
||||
"""
|
||||
self.model_class = model_class
|
||||
self.key_name = key_name
|
||||
self.key_value = key_value
|
||||
self.property_name = property_name
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from datastore.
|
||||
|
||||
Returns:
|
||||
oauth2client.Credentials
|
||||
"""
|
||||
credential = None
|
||||
|
||||
query = {self.key_name: self.key_value}
|
||||
entities = self.model_class.objects.filter(**query)
|
||||
if len(entities) > 0:
|
||||
credential = getattr(entities[0], self.property_name)
|
||||
if credential and hasattr(credential, 'set_store'):
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials, overwrite=False):
|
||||
"""Write a Credentials to the datastore.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
overwrite: Boolean, indicates whether you would like these credentials to
|
||||
overwrite any existing stored credentials.
|
||||
"""
|
||||
args = {self.key_name: self.key_value}
|
||||
|
||||
if overwrite:
|
||||
entity, unused_is_new = self.model_class.objects.get_or_create(**args)
|
||||
else:
|
||||
entity = self.model_class(**args)
|
||||
|
||||
setattr(entity, self.property_name, credentials)
|
||||
entity.save()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials from the datastore."""
|
||||
|
||||
query = {self.key_name: self.key_value}
|
||||
entities = self.model_class.objects.filter(**query).delete()
|
||||
@@ -1,122 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for OAuth.
|
||||
|
||||
Utilities for making it easier to work with OAuth 2.0
|
||||
credentials.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
|
||||
|
||||
class CredentialsFileSymbolicLinkError(Exception):
|
||||
"""Credentials files must not be symbolic links."""
|
||||
|
||||
|
||||
class Storage(BaseStorage):
|
||||
"""Store and retrieve a single credential to and from a file."""
|
||||
|
||||
def __init__(self, filename):
|
||||
self._filename = filename
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def _validate_file(self):
|
||||
if os.path.islink(self._filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: %s is a symbolic link.' % self._filename)
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant."""
|
||||
self._lock.acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._lock.release()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
credentials = None
|
||||
self._validate_file()
|
||||
try:
|
||||
f = open(self._filename, 'rb')
|
||||
content = f.read()
|
||||
f.close()
|
||||
except IOError:
|
||||
return credentials
|
||||
|
||||
try:
|
||||
credentials = Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
def _create_file_if_needed(self):
|
||||
"""Create an empty file if necessary.
|
||||
|
||||
This method will not initialize the file. Instead it implements a
|
||||
simple version of "touch" to ensure the file has been created.
|
||||
"""
|
||||
if not os.path.exists(self._filename):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._filename, 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
|
||||
Raises:
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
|
||||
self._create_file_if_needed()
|
||||
self._validate_file()
|
||||
f = open(self._filename, 'w')
|
||||
f.write(credentials.to_json())
|
||||
f.close()
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
|
||||
os.unlink(self._filename)
|
||||
@@ -1,105 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Utilities for Google Compute Engine
|
||||
|
||||
Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import json
|
||||
import logging
|
||||
from six.moves import urllib
|
||||
|
||||
from oauth2client import util
|
||||
from oauth2client.client import AccessTokenRefreshError
|
||||
from oauth2client.client import AssertionCredentials
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# URI Template for the endpoint that returns access_tokens.
|
||||
META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/'
|
||||
'default/acquire{?scope}')
|
||||
|
||||
|
||||
class AppAssertionCredentials(AssertionCredentials):
|
||||
"""Credentials object for Compute Engine Assertion Grants
|
||||
|
||||
This object will allow a Compute Engine instance to identify itself to
|
||||
Google and other OAuth 2.0 servers that can verify assertions. It can be used
|
||||
for the purpose of accessing data stored under an account assigned to the
|
||||
Compute Engine instance itself.
|
||||
|
||||
This credential does not require a flow to instantiate because it represents
|
||||
a two legged flow, and therefore has all of the required information to
|
||||
generate and refresh its own access tokens.
|
||||
"""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, scope, **kwargs):
|
||||
"""Constructor for AppAssertionCredentials
|
||||
|
||||
Args:
|
||||
scope: string or iterable of strings, scope(s) of the credentials being
|
||||
requested.
|
||||
"""
|
||||
self.scope = util.scopes_to_string(scope)
|
||||
self.kwargs = kwargs
|
||||
|
||||
# Assertion type is no longer used, but still in the parent class signature.
|
||||
super(AppAssertionCredentials, self).__init__(None)
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, json_data):
|
||||
data = json.loads(json_data)
|
||||
return AppAssertionCredentials(data['scope'])
|
||||
|
||||
def _refresh(self, http_request):
|
||||
"""Refreshes the access_token.
|
||||
|
||||
Skip all the storage hoops and just refresh using the API.
|
||||
|
||||
Args:
|
||||
http_request: callable, a callable that matches the method signature of
|
||||
httplib2.Http.request, used to make the refresh request.
|
||||
|
||||
Raises:
|
||||
AccessTokenRefreshError: When the refresh fails.
|
||||
"""
|
||||
query = '?scope=%s' % urllib.parse.quote(self.scope, '')
|
||||
uri = META.replace('{?scope}', query)
|
||||
response, content = http_request(uri)
|
||||
if response.status == 200:
|
||||
try:
|
||||
d = json.loads(content)
|
||||
except Exception as e:
|
||||
raise AccessTokenRefreshError(str(e))
|
||||
self.access_token = d['accessToken']
|
||||
else:
|
||||
if response.status == 404:
|
||||
content += (' This can occur if a VM was created'
|
||||
' with no service account or scopes.')
|
||||
raise AccessTokenRefreshError(content)
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
raise NotImplementedError(
|
||||
'Cannot serialize credentials for GCE service accounts.')
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self.scope
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return AppAssertionCredentials(scopes, **self.kwargs)
|
||||
@@ -1,110 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A keyring based Storage.
|
||||
|
||||
A Storage for Credentials that uses the keyring module.
|
||||
"""
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
|
||||
import threading
|
||||
|
||||
import keyring
|
||||
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
|
||||
|
||||
class Storage(BaseStorage):
|
||||
"""Store and retrieve a single credential to and from the keyring.
|
||||
|
||||
To use this module you must have the keyring module installed. See
|
||||
<http://pypi.python.org/pypi/keyring/>. This is an optional module and is not
|
||||
installed with oauth2client by default because it does not work on all the
|
||||
platforms that oauth2client supports, such as Google App Engine.
|
||||
|
||||
The keyring module <http://pypi.python.org/pypi/keyring/> is a cross-platform
|
||||
library for access the keyring capabilities of the local system. The user will
|
||||
be prompted for their keyring password when this module is used, and the
|
||||
manner in which the user is prompted will vary per platform.
|
||||
|
||||
Usage:
|
||||
from oauth2client.keyring_storage import Storage
|
||||
|
||||
s = Storage('name_of_application', 'user1')
|
||||
credentials = s.get()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, service_name, user_name):
|
||||
"""Constructor.
|
||||
|
||||
Args:
|
||||
service_name: string, The name of the service under which the credentials
|
||||
are stored.
|
||||
user_name: string, The name of the user to store credentials for.
|
||||
"""
|
||||
self._service_name = service_name
|
||||
self._user_name = user_name
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant."""
|
||||
self._lock.acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._lock.release()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve Credential from file.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credentials = None
|
||||
content = keyring.get_password(self._service_name, self._user_name)
|
||||
|
||||
if content is not None:
|
||||
try:
|
||||
credentials = Credentials.new_from_json(content)
|
||||
credentials.set_store(self)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return credentials
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write Credentials to file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name,
|
||||
credentials.to_json())
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete Credentials file.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
keyring.set_password(self._service_name, self._user_name, '')
|
||||
@@ -1,378 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Locked file interface that should work on Unix and Windows pythons.
|
||||
|
||||
This module first tries to use fcntl locking to ensure serialized access
|
||||
to a file, then falls back on a lock file if that is unavialable.
|
||||
|
||||
Usage::
|
||||
|
||||
f = LockedFile('filename', 'r+b', 'rb')
|
||||
f.open_and_lock()
|
||||
if f.is_locked():
|
||||
print('Acquired filename with r+b mode')
|
||||
f.file_handle().write('locked data')
|
||||
else:
|
||||
print('Acquired filename with rb mode')
|
||||
f.unlock_and_close()
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
__author__ = 'cache@google.com (David T McWherter)'
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from oauth2client import util
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CredentialsFileSymbolicLinkError(Exception):
|
||||
"""Credentials files must not be symbolic links."""
|
||||
|
||||
|
||||
class AlreadyLockedException(Exception):
|
||||
"""Trying to lock a file that has already been locked by the LockedFile."""
|
||||
pass
|
||||
|
||||
|
||||
def validate_file(filename):
|
||||
if os.path.islink(filename):
|
||||
raise CredentialsFileSymbolicLinkError(
|
||||
'File: %s is a symbolic link.' % filename)
|
||||
|
||||
class _Opener(object):
|
||||
"""Base class for different locking primitives."""
|
||||
|
||||
def __init__(self, filename, mode, fallback_mode):
|
||||
"""Create an Opener.
|
||||
|
||||
Args:
|
||||
filename: string, The pathname of the file.
|
||||
mode: string, The preferred mode to access the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
"""
|
||||
self._locked = False
|
||||
self._filename = filename
|
||||
self._mode = mode
|
||||
self._fallback_mode = fallback_mode
|
||||
self._fh = None
|
||||
self._lock_fd = None
|
||||
|
||||
def is_locked(self):
|
||||
"""Was the file locked."""
|
||||
return self._locked
|
||||
|
||||
def file_handle(self):
|
||||
"""The file handle to the file. Valid only after opened."""
|
||||
return self._fh
|
||||
|
||||
def filename(self):
|
||||
"""The filename that is being locked."""
|
||||
return self._filename
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
"""
|
||||
pass
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close the file."""
|
||||
pass
|
||||
|
||||
|
||||
class _PosixOpener(_Opener):
|
||||
"""Lock files using Posix advisory lock files."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Tries to create a .lock file next to the file we're trying to open.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise AlreadyLockedException('File %s is already locked' %
|
||||
self._filename)
|
||||
self._locked = False
|
||||
|
||||
validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
start_time = time.time()
|
||||
while True:
|
||||
try:
|
||||
self._lock_fd = os.open(lock_filename,
|
||||
os.O_CREAT|os.O_EXCL|os.O_RDWR)
|
||||
self._locked = True
|
||||
break
|
||||
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
if (time.time() - start_time) >= timeout:
|
||||
logger.warn('Could not acquire lock %s in %s seconds',
|
||||
lock_filename, timeout)
|
||||
# Close the file and open in fallback_mode.
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock a file by removing the .lock file, and close the handle."""
|
||||
if self._locked:
|
||||
lock_filename = self._posix_lockfile(self._filename)
|
||||
os.close(self._lock_fd)
|
||||
os.unlink(lock_filename)
|
||||
self._locked = False
|
||||
self._lock_fd = None
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
|
||||
def _posix_lockfile(self, filename):
|
||||
"""The name of the lock file to use for posix locking."""
|
||||
return '%s.lock' % filename
|
||||
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
|
||||
class _FcntlOpener(_Opener):
|
||||
"""Open, lock, and unlock a file using fcntl.lockf."""
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise AlreadyLockedException('File %s is already locked' %
|
||||
self._filename)
|
||||
start_time = time.time()
|
||||
|
||||
validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and don't lock.
|
||||
if e.errno in (errno.EPERM, errno.EACCES):
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
|
||||
self._locked = True
|
||||
return
|
||||
except IOError as e:
|
||||
# If not retrying, then just pass on the error.
|
||||
if timeout == 0:
|
||||
raise
|
||||
if e.errno != errno.EACCES:
|
||||
raise
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
logger.warn('Could not lock %s in %s seconds',
|
||||
self._filename, timeout)
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the fcntl.lockf primitive."""
|
||||
if self._locked:
|
||||
fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
except ImportError:
|
||||
_FcntlOpener = None
|
||||
|
||||
|
||||
try:
|
||||
import pywintypes
|
||||
import win32con
|
||||
import win32file
|
||||
|
||||
class _Win32Opener(_Opener):
|
||||
"""Open, lock, and unlock a file using windows primitives."""
|
||||
|
||||
# Error #33:
|
||||
# 'The process cannot access the file because another process'
|
||||
FILE_IN_USE_ERROR = 33
|
||||
|
||||
# Error #158:
|
||||
# 'The segment is already unlocked.'
|
||||
FILE_ALREADY_UNLOCKED_ERROR = 158
|
||||
|
||||
def open_and_lock(self, timeout, delay):
|
||||
"""Open the file and lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, How long to try to lock for.
|
||||
delay: float, How long to wait between retries
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
CredentialsFileSymbolicLinkError if the file is a symbolic link.
|
||||
"""
|
||||
if self._locked:
|
||||
raise AlreadyLockedException('File %s is already locked' %
|
||||
self._filename)
|
||||
start_time = time.time()
|
||||
|
||||
validate_file(self._filename)
|
||||
try:
|
||||
self._fh = open(self._filename, self._mode)
|
||||
except IOError as e:
|
||||
# If we can't access with _mode, try _fallback_mode and don't lock.
|
||||
if e.errno == errno.EACCES:
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
|
||||
# We opened in _mode, try to lock the file.
|
||||
while True:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.LockFileEx(
|
||||
hfile,
|
||||
(win32con.LOCKFILE_FAIL_IMMEDIATELY|
|
||||
win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
|
||||
pywintypes.OVERLAPPED())
|
||||
self._locked = True
|
||||
return
|
||||
except pywintypes.error as e:
|
||||
if timeout == 0:
|
||||
raise
|
||||
|
||||
# If the error is not that the file is already in use, raise.
|
||||
if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
|
||||
raise
|
||||
|
||||
# We could not acquire the lock. Try again.
|
||||
if (time.time() - start_time) >= timeout:
|
||||
logger.warn('Could not lock %s in %s seconds' % (
|
||||
self._filename, timeout))
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
self._fh = open(self._filename, self._fallback_mode)
|
||||
return
|
||||
time.sleep(delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Close and unlock the file using the win32 primitive."""
|
||||
if self._locked:
|
||||
try:
|
||||
hfile = win32file._get_osfhandle(self._fh.fileno())
|
||||
win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
|
||||
except pywintypes.error as e:
|
||||
if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
|
||||
raise
|
||||
self._locked = False
|
||||
if self._fh:
|
||||
self._fh.close()
|
||||
except ImportError:
|
||||
_Win32Opener = None
|
||||
|
||||
|
||||
class LockedFile(object):
|
||||
"""Represent a file that has exclusive access."""
|
||||
|
||||
@util.positional(4)
|
||||
def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
|
||||
"""Construct a LockedFile.
|
||||
|
||||
Args:
|
||||
filename: string, The path of the file to open.
|
||||
mode: string, The mode to try to open the file with.
|
||||
fallback_mode: string, The mode to use if locking fails.
|
||||
use_native_locking: bool, Whether or not fcntl/win32 locking is used.
|
||||
"""
|
||||
opener = None
|
||||
if not opener and use_native_locking:
|
||||
if _Win32Opener:
|
||||
opener = _Win32Opener(filename, mode, fallback_mode)
|
||||
if _FcntlOpener:
|
||||
opener = _FcntlOpener(filename, mode, fallback_mode)
|
||||
|
||||
if not opener:
|
||||
opener = _PosixOpener(filename, mode, fallback_mode)
|
||||
|
||||
self._opener = opener
|
||||
|
||||
def filename(self):
|
||||
"""Return the filename we were constructed with."""
|
||||
return self._opener._filename
|
||||
|
||||
def file_handle(self):
|
||||
"""Return the file_handle to the opened file."""
|
||||
return self._opener.file_handle()
|
||||
|
||||
def is_locked(self):
|
||||
"""Return whether we successfully locked the file."""
|
||||
return self._opener.is_locked()
|
||||
|
||||
def open_and_lock(self, timeout=0, delay=0.05):
|
||||
"""Open the file, trying to lock it.
|
||||
|
||||
Args:
|
||||
timeout: float, The number of seconds to try to acquire the lock.
|
||||
delay: float, The number of seconds to wait between retry attempts.
|
||||
|
||||
Raises:
|
||||
AlreadyLockedException: if the lock is already acquired.
|
||||
IOError: if the open fails.
|
||||
"""
|
||||
self._opener.open_and_lock(timeout, delay)
|
||||
|
||||
def unlock_and_close(self):
|
||||
"""Unlock and close a file."""
|
||||
self._opener.unlock_and_close()
|
||||
@@ -1,475 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Multi-credential file store with lock support.
|
||||
|
||||
This module implements a JSON credential store where multiple
|
||||
credentials can be stored in one file. That file supports locking
|
||||
both in a single process and across processes.
|
||||
|
||||
The credential themselves are keyed off of:
|
||||
|
||||
* client_id
|
||||
* user_agent
|
||||
* scope
|
||||
|
||||
The format of the stored data is like so::
|
||||
|
||||
{
|
||||
'file_version': 1,
|
||||
'data': [
|
||||
{
|
||||
'key': {
|
||||
'clientId': '<client id>',
|
||||
'userAgent': '<user agent>',
|
||||
'scope': '<scope>'
|
||||
},
|
||||
'credential': {
|
||||
# JSON serialized Credentials.
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
__author__ = 'jbeda@google.com (Joe Beda)'
|
||||
|
||||
import errno
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
|
||||
from oauth2client.client import Credentials
|
||||
from oauth2client.client import Storage as BaseStorage
|
||||
from oauth2client import util
|
||||
from oauth2client.locked_file import LockedFile
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# A dict from 'filename'->_MultiStore instances
|
||||
_multistores = {}
|
||||
_multistores_lock = threading.Lock()
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Base error for this module."""
|
||||
|
||||
|
||||
class NewerCredentialStoreError(Error):
|
||||
"""The credential store is a newer version than supported."""
|
||||
|
||||
|
||||
@util.positional(4)
|
||||
def get_credential_storage(filename, client_id, user_agent, scope,
|
||||
warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
client_id: The client_id for the credential
|
||||
user_agent: The user agent for the credential
|
||||
scope: string or iterable of strings, Scope(s) being requested
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Recreate the legacy key with these specific parameters
|
||||
key = {'clientId': client_id, 'userAgent': user_agent,
|
||||
'scope': util.scopes_to_string(scope)}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_string_key(
|
||||
filename, key_string, warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a single string as a key.
|
||||
|
||||
Allows you to provide a string as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_string: A string to use as the key for storing this credential.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
# Create a key dictionary that can be used
|
||||
key_dict = {'key': key_string}
|
||||
return get_credential_storage_custom_key(
|
||||
filename, key_dict, warn_on_readonly=warn_on_readonly)
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def get_credential_storage_custom_key(
|
||||
filename, key_dict, warn_on_readonly=True):
|
||||
"""Get a Storage instance for a credential using a dictionary as a key.
|
||||
|
||||
Allows you to provide a dictionary as a custom key that will be used for
|
||||
credential storage and retrieval.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
key_dict: A dictionary to use as the key for storing this credential. There
|
||||
is no ordering of the keys in the dictionary. Logically equivalent
|
||||
dictionaries will produce equivalent storage keys.
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
An object derived from client.Storage for getting/setting the
|
||||
credential.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
key = util.dict_to_tuple_key(key_dict)
|
||||
return multistore._get_storage(key)
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def get_all_credential_keys(filename, warn_on_readonly=True):
|
||||
"""Gets all the registered credential keys in the given Multistore.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A list of the credential keys present in the file. They are returned as
|
||||
dictionaries that can be passed into get_credential_storage_custom_key to
|
||||
get the actual credentials.
|
||||
"""
|
||||
multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
|
||||
multistore._lock()
|
||||
try:
|
||||
return multistore._get_all_credential_keys()
|
||||
finally:
|
||||
multistore._unlock()
|
||||
|
||||
|
||||
@util.positional(1)
|
||||
def _get_multistore(filename, warn_on_readonly=True):
|
||||
"""A helper method to initialize the multistore with proper locking.
|
||||
|
||||
Args:
|
||||
filename: The JSON file storing a set of credentials
|
||||
warn_on_readonly: if True, log a warning if the store is readonly
|
||||
|
||||
Returns:
|
||||
A multistore object
|
||||
"""
|
||||
filename = os.path.expanduser(filename)
|
||||
_multistores_lock.acquire()
|
||||
try:
|
||||
multistore = _multistores.setdefault(
|
||||
filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
|
||||
finally:
|
||||
_multistores_lock.release()
|
||||
return multistore
|
||||
|
||||
|
||||
class _MultiStore(object):
|
||||
"""A file backed store for multiple credentials."""
|
||||
|
||||
@util.positional(2)
|
||||
def __init__(self, filename, warn_on_readonly=True):
|
||||
"""Initialize the class.
|
||||
|
||||
This will create the file if necessary.
|
||||
"""
|
||||
self._file = LockedFile(filename, 'r+', 'r')
|
||||
self._thread_lock = threading.Lock()
|
||||
self._read_only = False
|
||||
self._warn_on_readonly = warn_on_readonly
|
||||
|
||||
self._create_file_if_needed()
|
||||
|
||||
# Cache of deserialized store. This is only valid after the
|
||||
# _MultiStore is locked or _refresh_data_cache is called. This is
|
||||
# of the form of:
|
||||
#
|
||||
# ((key, value), (key, value)...) -> OAuth2Credential
|
||||
#
|
||||
# If this is None, then the store hasn't been read yet.
|
||||
self._data = None
|
||||
|
||||
class _Storage(BaseStorage):
|
||||
"""A Storage object that knows how to read/write a single credential."""
|
||||
|
||||
def __init__(self, multistore, key):
|
||||
self._multistore = multistore
|
||||
self._key = key
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquires any lock necessary to access this Storage.
|
||||
|
||||
This lock is not reentrant.
|
||||
"""
|
||||
self._multistore._lock()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the Storage lock.
|
||||
|
||||
Trying to release a lock that isn't held will result in a
|
||||
RuntimeError.
|
||||
"""
|
||||
self._multistore._unlock()
|
||||
|
||||
def locked_get(self):
|
||||
"""Retrieve credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Returns:
|
||||
oauth2client.client.Credentials
|
||||
"""
|
||||
credential = self._multistore._get_credential(self._key)
|
||||
if credential:
|
||||
credential.set_store(self)
|
||||
return credential
|
||||
|
||||
def locked_put(self, credentials):
|
||||
"""Write a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._update_credential(self._key, credentials)
|
||||
|
||||
def locked_delete(self):
|
||||
"""Delete a credential.
|
||||
|
||||
The Storage lock must be held when this is called.
|
||||
|
||||
Args:
|
||||
credentials: Credentials, the credentials to store.
|
||||
"""
|
||||
self._multistore._delete_credential(self._key)
|
||||
|
||||
def _create_file_if_needed(self):
|
||||
"""Create an empty file if necessary.
|
||||
|
||||
This method will not initialize the file. Instead it implements a
|
||||
simple version of "touch" to ensure the file has been created.
|
||||
"""
|
||||
if not os.path.exists(self._file.filename()):
|
||||
old_umask = os.umask(0o177)
|
||||
try:
|
||||
open(self._file.filename(), 'a+b').close()
|
||||
finally:
|
||||
os.umask(old_umask)
|
||||
|
||||
def _lock(self):
|
||||
"""Lock the entire multistore."""
|
||||
self._thread_lock.acquire()
|
||||
try:
|
||||
self._file.open_and_lock()
|
||||
except IOError as e:
|
||||
if e.errno == errno.ENOSYS:
|
||||
logger.warn('File system does not support locking the credentials '
|
||||
'file.')
|
||||
elif e.errno == errno.ENOLCK:
|
||||
logger.warn('File system is out of resources for writing the '
|
||||
'credentials file (is your disk full?).')
|
||||
else:
|
||||
raise
|
||||
if not self._file.is_locked():
|
||||
self._read_only = True
|
||||
if self._warn_on_readonly:
|
||||
logger.warn('The credentials file (%s) is not writable. Opening in '
|
||||
'read-only mode. Any refreshed credentials will only be '
|
||||
'valid for this run.', self._file.filename())
|
||||
if os.path.getsize(self._file.filename()) == 0:
|
||||
logger.debug('Initializing empty multistore file')
|
||||
# The multistore is empty so write out an empty file.
|
||||
self._data = {}
|
||||
self._write()
|
||||
elif not self._read_only or self._data is None:
|
||||
# Only refresh the data if we are read/write or we haven't
|
||||
# cached the data yet. If we are readonly, we assume is isn't
|
||||
# changing out from under us and that we only have to read it
|
||||
# once. This prevents us from whacking any new access keys that
|
||||
# we have cached in memory but were unable to write out.
|
||||
self._refresh_data_cache()
|
||||
|
||||
def _unlock(self):
|
||||
"""Release the lock on the multistore."""
|
||||
self._file.unlock_and_close()
|
||||
self._thread_lock.release()
|
||||
|
||||
def _locked_json_read(self):
|
||||
"""Get the raw content of the multistore file.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Returns:
|
||||
The contents of the multistore decoded as JSON.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
self._file.file_handle().seek(0)
|
||||
return json.load(self._file.file_handle())
|
||||
|
||||
def _locked_json_write(self, data):
|
||||
"""Write a JSON serializable data structure to the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Args:
|
||||
data: The data to be serialized and written.
|
||||
"""
|
||||
assert self._thread_lock.locked()
|
||||
if self._read_only:
|
||||
return
|
||||
self._file.file_handle().seek(0)
|
||||
json.dump(data, self._file.file_handle(), sort_keys=True, indent=2, separators=(',', ': '))
|
||||
self._file.file_handle().truncate()
|
||||
|
||||
def _refresh_data_cache(self):
|
||||
"""Refresh the contents of the multistore.
|
||||
|
||||
The multistore must be locked when this is called.
|
||||
|
||||
Raises:
|
||||
NewerCredentialStoreError: Raised when a newer client has written the
|
||||
store.
|
||||
"""
|
||||
self._data = {}
|
||||
try:
|
||||
raw_data = self._locked_json_read()
|
||||
except Exception:
|
||||
logger.warn('Credential data store could not be loaded. '
|
||||
'Will ignore and overwrite.')
|
||||
return
|
||||
|
||||
version = 0
|
||||
try:
|
||||
version = raw_data['file_version']
|
||||
except Exception:
|
||||
logger.warn('Missing version for credential data store. It may be '
|
||||
'corrupt or an old version. Overwriting.')
|
||||
if version > 1:
|
||||
raise NewerCredentialStoreError(
|
||||
'Credential file has file_version of %d. '
|
||||
'Only file_version of 1 is supported.' % version)
|
||||
|
||||
credentials = []
|
||||
try:
|
||||
credentials = raw_data['data']
|
||||
except (TypeError, KeyError):
|
||||
pass
|
||||
|
||||
for cred_entry in credentials:
|
||||
try:
|
||||
(key, credential) = self._decode_credential_from_json(cred_entry)
|
||||
self._data[key] = credential
|
||||
except:
|
||||
# If something goes wrong loading a credential, just ignore it
|
||||
logger.info('Error decoding credential, skipping', exc_info=True)
|
||||
|
||||
def _decode_credential_from_json(self, cred_entry):
|
||||
"""Load a credential from our JSON serialization.
|
||||
|
||||
Args:
|
||||
cred_entry: A dict entry from the data member of our format
|
||||
|
||||
Returns:
|
||||
(key, cred) where the key is the key tuple and the cred is the
|
||||
OAuth2Credential object.
|
||||
"""
|
||||
raw_key = cred_entry['key']
|
||||
key = util.dict_to_tuple_key(raw_key)
|
||||
credential = None
|
||||
credential = Credentials.new_from_json(json.dumps(cred_entry['credential']))
|
||||
return (key, credential)
|
||||
|
||||
def _write(self):
|
||||
"""Write the cached data back out.
|
||||
|
||||
The multistore must be locked.
|
||||
"""
|
||||
raw_data = {'file_version': 1}
|
||||
raw_creds = []
|
||||
raw_data['data'] = raw_creds
|
||||
for (cred_key, cred) in self._data.items():
|
||||
raw_key = dict(cred_key)
|
||||
raw_cred = json.loads(cred.to_json())
|
||||
raw_creds.append({'key': raw_key, 'credential': raw_cred})
|
||||
self._locked_json_write(raw_data)
|
||||
|
||||
def _get_all_credential_keys(self):
|
||||
"""Gets all the registered credential keys in the multistore.
|
||||
|
||||
Returns:
|
||||
A list of dictionaries corresponding to all the keys currently registered
|
||||
"""
|
||||
return [dict(key) for key in self._data.keys()]
|
||||
|
||||
def _get_credential(self, key):
|
||||
"""Get a credential from the multistore.
|
||||
|
||||
The multistore must be locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
The credential specified or None if not present
|
||||
"""
|
||||
return self._data.get(key, None)
|
||||
|
||||
def _update_credential(self, key, cred):
|
||||
"""Update a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
cred: The OAuth2Credential to update/set
|
||||
"""
|
||||
self._data[key] = cred
|
||||
self._write()
|
||||
|
||||
def _delete_credential(self, key):
|
||||
"""Delete a credential and write the multistore.
|
||||
|
||||
This must be called when the multistore is locked.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
"""
|
||||
try:
|
||||
del self._data[key]
|
||||
except KeyError:
|
||||
pass
|
||||
self._write()
|
||||
|
||||
def _get_storage(self, key):
|
||||
"""Get a Storage object to get/set a credential.
|
||||
|
||||
This Storage is a 'view' into the multistore.
|
||||
|
||||
Args:
|
||||
key: The key used to retrieve the credential
|
||||
|
||||
Returns:
|
||||
A Storage object that can be used to get/set this cred
|
||||
"""
|
||||
return self._Storage(self, key)
|
||||
@@ -1,161 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module holds the old run() function which is deprecated, the
|
||||
tools.run_flow() function should be used in its place."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import webbrowser
|
||||
|
||||
import gflags
|
||||
from six.moves import input
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
from oauth2client.tools import ClientRedirectHandler
|
||||
from oauth2client.tools import ClientRedirectServer
|
||||
|
||||
|
||||
FLAGS = gflags.FLAGS
|
||||
|
||||
gflags.DEFINE_boolean('auth_local_webserver', True,
|
||||
('Run a local web server to handle redirects during '
|
||||
'OAuth authorization.'))
|
||||
|
||||
gflags.DEFINE_string('auth_host_name', 'localhost',
|
||||
('Host name to use when running a local web server to '
|
||||
'handle redirects during OAuth authorization.'))
|
||||
|
||||
gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
|
||||
('Port to use when running a local web server to '
|
||||
'handle redirects during OAuth authorization.'))
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def run(flow, storage, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
through all the steps to obtain credentials. It takes a ``Flow``
|
||||
argument and attempts to open an authorization server page in the
|
||||
user's default web browser. The server asks the user to grant your
|
||||
application access to the user's data. If the user grants access,
|
||||
the ``run()`` function returns new credentials. The new credentials
|
||||
are also stored in the ``storage`` argument, which updates the file
|
||||
associated with the ``Storage`` object.
|
||||
|
||||
It presumes it is run from a command-line application and supports the
|
||||
following flags:
|
||||
|
||||
``--auth_host_name`` (string, default: ``localhost``)
|
||||
Host name to use when running a local web server to handle
|
||||
redirects during OAuth authorization.
|
||||
|
||||
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
|
||||
Port to use when running a local web server to handle redirects
|
||||
during OAuth authorization. Repeat this option to specify a list
|
||||
of values.
|
||||
|
||||
``--[no]auth_local_webserver`` (boolean, default: ``True``)
|
||||
Run a local web server to handle redirects during OAuth authorization.
|
||||
|
||||
Since it uses flags make sure to initialize the ``gflags`` module before
|
||||
calling ``run()``.
|
||||
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
http: An instance of ``httplib2.Http.request`` or something that acts
|
||||
like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
logging.warning('This function, oauth2client.tools.run(), and the use of '
|
||||
'the gflags library are deprecated and will be removed in a future '
|
||||
'version of the library.')
|
||||
if FLAGS.auth_local_webserver:
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in FLAGS.auth_host_port:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
|
||||
ClientRedirectHandler)
|
||||
except socket.error as e:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
FLAGS.auth_local_webserver = success
|
||||
if not success:
|
||||
print('Failed to start a local webserver listening on either port 8080')
|
||||
print('or port 9090. Please check your firewall settings and locally')
|
||||
print('running programs that may be blocking or using those ports.')
|
||||
print()
|
||||
print('Falling back to --noauth_local_webserver and continuing with')
|
||||
print('authorization.')
|
||||
print()
|
||||
|
||||
if FLAGS.auth_local_webserver:
|
||||
oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
|
||||
else:
|
||||
oauth_callback = client.OOB_CALLBACK_URN
|
||||
flow.redirect_uri = oauth_callback
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
|
||||
if FLAGS.auth_local_webserver:
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
print('Your browser has been opened to visit:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
print('If your browser is on a different machine then exit and re-run')
|
||||
print('this application with the command-line parameter ')
|
||||
print()
|
||||
print(' --noauth_local_webserver')
|
||||
print()
|
||||
else:
|
||||
print('Go to the following link in your browser:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
|
||||
code = None
|
||||
if FLAGS.auth_local_webserver:
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
sys.exit('Authentication request was rejected.')
|
||||
if 'code' in httpd.query_params:
|
||||
code = httpd.query_params['code']
|
||||
else:
|
||||
print('Failed to find "code" in the query parameters of the redirect.')
|
||||
sys.exit('Try running with --noauth_local_webserver.')
|
||||
else:
|
||||
code = input('Enter verification code: ').strip()
|
||||
|
||||
try:
|
||||
credential = flow.step2_exchange(code, http=http)
|
||||
except client.FlowExchangeError as e:
|
||||
sys.exit('Authentication has failed: %s' % e)
|
||||
|
||||
storage.put(credential)
|
||||
credential.set_store(storage)
|
||||
print('Authentication successful.')
|
||||
|
||||
return credential
|
||||
@@ -1,139 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""A service account credentials class.
|
||||
|
||||
This credentials class is implemented on top of rsa library.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import six
|
||||
import time
|
||||
|
||||
from pyasn1.codec.ber import decoder
|
||||
from pyasn1_modules.rfc5208 import PrivateKeyInfo
|
||||
import rsa
|
||||
|
||||
from oauth2client import GOOGLE_REVOKE_URI
|
||||
from oauth2client import GOOGLE_TOKEN_URI
|
||||
from oauth2client import util
|
||||
from oauth2client.client import AssertionCredentials
|
||||
|
||||
|
||||
class _ServiceAccountCredentials(AssertionCredentials):
|
||||
"""Class representing a service account (signed JWT) credential."""
|
||||
|
||||
MAX_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
|
||||
|
||||
def __init__(self, service_account_id, service_account_email, private_key_id,
|
||||
private_key_pkcs8_text, scopes, user_agent=None,
|
||||
token_uri=GOOGLE_TOKEN_URI, revoke_uri=GOOGLE_REVOKE_URI,
|
||||
**kwargs):
|
||||
|
||||
super(_ServiceAccountCredentials, self).__init__(
|
||||
None, user_agent=user_agent, token_uri=token_uri, revoke_uri=revoke_uri)
|
||||
|
||||
self._service_account_id = service_account_id
|
||||
self._service_account_email = service_account_email
|
||||
self._private_key_id = private_key_id
|
||||
self._private_key = _get_private_key(private_key_pkcs8_text)
|
||||
self._private_key_pkcs8_text = private_key_pkcs8_text
|
||||
self._scopes = util.scopes_to_string(scopes)
|
||||
self._user_agent = user_agent
|
||||
self._token_uri = token_uri
|
||||
self._revoke_uri = revoke_uri
|
||||
self._kwargs = kwargs
|
||||
|
||||
def _generate_assertion(self):
|
||||
"""Generate the assertion that will be used in the request."""
|
||||
|
||||
header = {
|
||||
'alg': 'RS256',
|
||||
'typ': 'JWT',
|
||||
'kid': self._private_key_id
|
||||
}
|
||||
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
'aud': self._token_uri,
|
||||
'scope': self._scopes,
|
||||
'iat': now,
|
||||
'exp': now + _ServiceAccountCredentials.MAX_TOKEN_LIFETIME_SECS,
|
||||
'iss': self._service_account_email
|
||||
}
|
||||
payload.update(self._kwargs)
|
||||
|
||||
assertion_input = (_urlsafe_b64encode(header) + b'.' +
|
||||
_urlsafe_b64encode(payload))
|
||||
|
||||
# Sign the assertion.
|
||||
rsa_bytes = rsa.pkcs1.sign(assertion_input, self._private_key, 'SHA-256')
|
||||
signature = base64.urlsafe_b64encode(rsa_bytes).rstrip(b'=')
|
||||
|
||||
return assertion_input + b'.' + signature
|
||||
|
||||
def sign_blob(self, blob):
|
||||
# Ensure that it is bytes
|
||||
try:
|
||||
blob = blob.encode('utf-8')
|
||||
except AttributeError:
|
||||
pass
|
||||
return (self._private_key_id,
|
||||
rsa.pkcs1.sign(blob, self._private_key, 'SHA-256'))
|
||||
|
||||
@property
|
||||
def service_account_email(self):
|
||||
return self._service_account_email
|
||||
|
||||
@property
|
||||
def serialization_data(self):
|
||||
return {
|
||||
'type': 'service_account',
|
||||
'client_id': self._service_account_id,
|
||||
'client_email': self._service_account_email,
|
||||
'private_key_id': self._private_key_id,
|
||||
'private_key': self._private_key_pkcs8_text
|
||||
}
|
||||
|
||||
def create_scoped_required(self):
|
||||
return not self._scopes
|
||||
|
||||
def create_scoped(self, scopes):
|
||||
return _ServiceAccountCredentials(self._service_account_id,
|
||||
self._service_account_email,
|
||||
self._private_key_id,
|
||||
self._private_key_pkcs8_text,
|
||||
scopes,
|
||||
user_agent=self._user_agent,
|
||||
token_uri=self._token_uri,
|
||||
revoke_uri=self._revoke_uri,
|
||||
**self._kwargs)
|
||||
|
||||
|
||||
def _urlsafe_b64encode(data):
|
||||
return base64.urlsafe_b64encode(
|
||||
json.dumps(data, separators=(',', ':')).encode('UTF-8')).rstrip(b'=')
|
||||
|
||||
|
||||
def _get_private_key(private_key_pkcs8_text):
|
||||
"""Get an RSA private key object from a pkcs8 representation."""
|
||||
|
||||
if not isinstance(private_key_pkcs8_text, six.binary_type):
|
||||
private_key_pkcs8_text = private_key_pkcs8_text.encode('ascii')
|
||||
der = rsa.pem.load_pem(private_key_pkcs8_text, 'PRIVATE KEY')
|
||||
asn1_private_key, _ = decoder.decode(der, asn1Spec=PrivateKeyInfo())
|
||||
return rsa.PrivateKey.load_pkcs1(
|
||||
asn1_private_key.getComponentByName('privateKey').asOctets(),
|
||||
format='DER')
|
||||
@@ -1,257 +0,0 @@
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Command-line tools for authenticating via OAuth 2.0
|
||||
|
||||
Do the OAuth 2.0 Web Server dance for a command line application. Stores the
|
||||
generated credentials in a common file that is used by other example apps in
|
||||
the same directory.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
__author__ = 'jcgregorio@google.com (Joe Gregorio)'
|
||||
__all__ = ['argparser', 'run_flow', 'run', 'message_if_missing']
|
||||
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
|
||||
from six.moves import BaseHTTPServer
|
||||
from six.moves import urllib
|
||||
from six.moves import input
|
||||
|
||||
from oauth2client import client
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
_CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
|
||||
|
||||
To make this sample run you will need to populate the client_secrets.json file
|
||||
found at:
|
||||
|
||||
%s
|
||||
|
||||
with information from the APIs Console <https://code.google.com/apis/console>.
|
||||
|
||||
"""
|
||||
|
||||
def _CreateArgumentParser():
|
||||
try:
|
||||
import argparse
|
||||
except ImportError:
|
||||
return None
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument('--auth_host_name', default='localhost',
|
||||
help='Hostname when running a local web server.')
|
||||
parser.add_argument('--noauth_local_webserver', action='store_true',
|
||||
default=False, help='Do not run a local web server.')
|
||||
parser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
|
||||
nargs='*', help='Port web server should listen on.')
|
||||
parser.add_argument('--logging_level', default='ERROR',
|
||||
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
|
||||
help='Set the logging level of detail.')
|
||||
return parser
|
||||
|
||||
# argparser is an ArgumentParser that contains command-line options expected
|
||||
# by tools.run(). Pass it in as part of the 'parents' argument to your own
|
||||
# ArgumentParser.
|
||||
argparser = _CreateArgumentParser()
|
||||
|
||||
|
||||
class ClientRedirectServer(BaseHTTPServer.HTTPServer):
|
||||
"""A server to handle OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into query_params and then stops serving.
|
||||
"""
|
||||
query_params = {}
|
||||
|
||||
|
||||
class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""A handler for OAuth 2.0 redirects back to localhost.
|
||||
|
||||
Waits for a single request and parses the query parameters
|
||||
into the servers query_params and then stops serving.
|
||||
"""
|
||||
|
||||
def do_GET(self):
|
||||
"""Handle a GET request.
|
||||
|
||||
Parses the query parameters and prints a message
|
||||
if the flow has completed. Note that we can't detect
|
||||
if an error occurred.
|
||||
"""
|
||||
self.send_response(200)
|
||||
self.send_header("Content-type", "text/html")
|
||||
self.end_headers()
|
||||
query = self.path.split('?', 1)[-1]
|
||||
query = dict(urllib.parse.parse_qsl(query))
|
||||
self.server.query_params = query
|
||||
self.wfile.write(b"<html><head><title>Authentication Status</title></head>")
|
||||
self.wfile.write(b"<body><p>The authentication flow has completed.</p>")
|
||||
self.wfile.write(b"</body></html>")
|
||||
|
||||
def log_message(self, format, *args):
|
||||
"""Do not log messages to stdout while running as command line program."""
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def run_flow(flow, storage, flags, http=None):
|
||||
"""Core code for a command-line application.
|
||||
|
||||
The ``run()`` function is called from your application and runs
|
||||
through all the steps to obtain credentials. It takes a ``Flow``
|
||||
argument and attempts to open an authorization server page in the
|
||||
user's default web browser. The server asks the user to grant your
|
||||
application access to the user's data. If the user grants access,
|
||||
the ``run()`` function returns new credentials. The new credentials
|
||||
are also stored in the ``storage`` argument, which updates the file
|
||||
associated with the ``Storage`` object.
|
||||
|
||||
It presumes it is run from a command-line application and supports the
|
||||
following flags:
|
||||
|
||||
``--auth_host_name`` (string, default: ``localhost``)
|
||||
Host name to use when running a local web server to handle
|
||||
redirects during OAuth authorization.
|
||||
|
||||
``--auth_host_port`` (integer, default: ``[8080, 8090]``)
|
||||
Port to use when running a local web server to handle redirects
|
||||
during OAuth authorization. Repeat this option to specify a list
|
||||
of values.
|
||||
|
||||
``--[no]auth_local_webserver`` (boolean, default: ``True``)
|
||||
Run a local web server to handle redirects during OAuth authorization.
|
||||
|
||||
|
||||
|
||||
|
||||
The tools module defines an ``ArgumentParser`` the already contains the flag
|
||||
definitions that ``run()`` requires. You can pass that ``ArgumentParser`` to your
|
||||
``ArgumentParser`` constructor::
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
parents=[tools.argparser])
|
||||
flags = parser.parse_args(argv)
|
||||
|
||||
Args:
|
||||
flow: Flow, an OAuth 2.0 Flow to step through.
|
||||
storage: Storage, a ``Storage`` to store the credential in.
|
||||
flags: ``argparse.Namespace``, The command-line flags. This is the
|
||||
object returned from calling ``parse_args()`` on
|
||||
``argparse.ArgumentParser`` as described above.
|
||||
http: An instance of ``httplib2.Http.request`` or something that
|
||||
acts like it.
|
||||
|
||||
Returns:
|
||||
Credentials, the obtained credential.
|
||||
"""
|
||||
logging.getLogger().setLevel(getattr(logging, flags.logging_level))
|
||||
if not flags.noauth_local_webserver:
|
||||
success = False
|
||||
port_number = 0
|
||||
for port in flags.auth_host_port:
|
||||
port_number = port
|
||||
try:
|
||||
httpd = ClientRedirectServer((flags.auth_host_name, port),
|
||||
ClientRedirectHandler)
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
success = True
|
||||
break
|
||||
flags.noauth_local_webserver = not success
|
||||
if not success:
|
||||
print('Failed to start a local webserver listening on either port 8080')
|
||||
print('or port 9090. Please check your firewall settings and locally')
|
||||
print('running programs that may be blocking or using those ports.')
|
||||
print()
|
||||
print('Falling back to --noauth_local_webserver and continuing with')
|
||||
print('authorization.')
|
||||
print()
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
|
||||
else:
|
||||
oauth_callback = client.OOB_CALLBACK_URN
|
||||
flow.redirect_uri = oauth_callback
|
||||
authorize_url = flow.step1_get_authorize_url()
|
||||
|
||||
if flags.short_url:
|
||||
try:
|
||||
from googleapiclient.discovery import build
|
||||
service = build('urlshortener', 'v1', http=http)
|
||||
url_result = service.url().insert(body={'longUrl': authorize_url},
|
||||
key=u'AIzaSyBlmgbii8QfJSYmC9VTMOfqrAt5Vj5wtzE').execute()
|
||||
authorize_url = url_result['id']
|
||||
except:
|
||||
pass
|
||||
|
||||
if not flags.noauth_local_webserver:
|
||||
import webbrowser
|
||||
webbrowser.open(authorize_url, new=1, autoraise=True)
|
||||
print('Your browser has been opened to visit:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
print('If your browser is on a different machine then exit and re-run this')
|
||||
print('after creating a file called nobrowser.txt in the same path as GAM.')
|
||||
print()
|
||||
else:
|
||||
print('Go to the following link in your browser:')
|
||||
print()
|
||||
print(' ' + authorize_url)
|
||||
print()
|
||||
|
||||
code = None
|
||||
if not flags.noauth_local_webserver:
|
||||
httpd.handle_request()
|
||||
if 'error' in httpd.query_params:
|
||||
sys.exit('Authentication request was rejected.')
|
||||
if 'code' in httpd.query_params:
|
||||
code = httpd.query_params['code']
|
||||
else:
|
||||
print('Failed to find "code" in the query parameters of the redirect.')
|
||||
sys.exit('Try running with --noauth_local_webserver.')
|
||||
else:
|
||||
code = input('Enter verification code: ').strip()
|
||||
|
||||
try:
|
||||
credential = flow.step2_exchange(code, http=http)
|
||||
except client.FlowExchangeError as e:
|
||||
sys.exit('Authentication has failed: %s' % e)
|
||||
|
||||
storage.put(credential)
|
||||
credential.set_store(storage)
|
||||
print('Authentication successful.')
|
||||
|
||||
return credential
|
||||
|
||||
|
||||
def message_if_missing(filename):
|
||||
"""Helpful message to display if the CLIENT_SECRETS file is missing."""
|
||||
|
||||
return _CLIENT_SECRETS_MESSAGE % filename
|
||||
|
||||
try:
|
||||
from oauth2client.old_run import run
|
||||
from oauth2client.old_run import FLAGS
|
||||
except ImportError:
|
||||
def run(*args, **kwargs):
|
||||
raise NotImplementedError(
|
||||
'The gflags library must be installed to use tools.run(). '
|
||||
'Please install gflags or preferrably switch to using '
|
||||
'tools.run_flow().')
|
||||
@@ -1,201 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2014 Google Inc. All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
|
||||
"""Common utility library."""
|
||||
|
||||
__author__ = [
|
||||
'rafek@google.com (Rafe Kaplan)',
|
||||
'guido@google.com (Guido van Rossum)',
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
'positional',
|
||||
'POSITIONAL_WARNING',
|
||||
'POSITIONAL_EXCEPTION',
|
||||
'POSITIONAL_IGNORE',
|
||||
]
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
|
||||
import six
|
||||
from six.moves import urllib
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
POSITIONAL_WARNING = 'WARNING'
|
||||
POSITIONAL_EXCEPTION = 'EXCEPTION'
|
||||
POSITIONAL_IGNORE = 'IGNORE'
|
||||
POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
|
||||
POSITIONAL_IGNORE])
|
||||
|
||||
positional_parameters_enforcement = POSITIONAL_WARNING
|
||||
|
||||
def positional(max_positional_args):
|
||||
"""A decorator to declare that only the first N arguments my be positional.
|
||||
|
||||
This decorator makes it easy to support Python 3 style keyword-only
|
||||
parameters. For example, in Python 3 it is possible to write::
|
||||
|
||||
def fn(pos1, *, kwonly1=None, kwonly1=None):
|
||||
...
|
||||
|
||||
All named parameters after ``*`` must be a keyword::
|
||||
|
||||
fn(10, 'kw1', 'kw2') # Raises exception.
|
||||
fn(10, kwonly1='kw1') # Ok.
|
||||
|
||||
Example
|
||||
^^^^^^^
|
||||
|
||||
To define a function like above, do::
|
||||
|
||||
@positional(1)
|
||||
def fn(pos1, kwonly1=None, kwonly2=None):
|
||||
...
|
||||
|
||||
If no default value is provided to a keyword argument, it becomes a required
|
||||
keyword argument::
|
||||
|
||||
@positional(0)
|
||||
def fn(required_kw):
|
||||
...
|
||||
|
||||
This must be called with the keyword parameter::
|
||||
|
||||
fn() # Raises exception.
|
||||
fn(10) # Raises exception.
|
||||
fn(required_kw=10) # Ok.
|
||||
|
||||
When defining instance or class methods always remember to account for
|
||||
``self`` and ``cls``::
|
||||
|
||||
class MyClass(object):
|
||||
|
||||
@positional(2)
|
||||
def my_method(self, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
@classmethod
|
||||
@positional(2)
|
||||
def my_method(cls, pos1, kwonly1=None):
|
||||
...
|
||||
|
||||
The positional decorator behavior is controlled by
|
||||
``util.positional_parameters_enforcement``, which may be set to
|
||||
``POSITIONAL_EXCEPTION``, ``POSITIONAL_WARNING`` or
|
||||
``POSITIONAL_IGNORE`` to raise an exception, log a warning, or do
|
||||
nothing, respectively, if a declaration is violated.
|
||||
|
||||
Args:
|
||||
max_positional_arguments: Maximum number of positional arguments. All
|
||||
parameters after the this index must be keyword only.
|
||||
|
||||
Returns:
|
||||
A decorator that prevents using arguments after max_positional_args from
|
||||
being used as positional parameters.
|
||||
|
||||
Raises:
|
||||
TypeError if a key-word only argument is provided as a positional
|
||||
parameter, but only if util.positional_parameters_enforcement is set to
|
||||
POSITIONAL_EXCEPTION.
|
||||
|
||||
"""
|
||||
def positional_decorator(wrapped):
|
||||
@functools.wraps(wrapped)
|
||||
def positional_wrapper(*args, **kwargs):
|
||||
if len(args) > max_positional_args:
|
||||
plural_s = ''
|
||||
if max_positional_args != 1:
|
||||
plural_s = 's'
|
||||
message = '%s() takes at most %d positional argument%s (%d given)' % (
|
||||
wrapped.__name__, max_positional_args, plural_s, len(args))
|
||||
if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
|
||||
raise TypeError(message)
|
||||
elif positional_parameters_enforcement == POSITIONAL_WARNING:
|
||||
logger.warning(message)
|
||||
else: # IGNORE
|
||||
pass
|
||||
return wrapped(*args, **kwargs)
|
||||
return positional_wrapper
|
||||
|
||||
if isinstance(max_positional_args, six.integer_types):
|
||||
return positional_decorator
|
||||
else:
|
||||
args, _, _, defaults = inspect.getargspec(max_positional_args)
|
||||
return positional(len(args) - len(defaults))(max_positional_args)
|
||||
|
||||
|
||||
def scopes_to_string(scopes):
|
||||
"""Converts scope value to a string.
|
||||
|
||||
If scopes is a string then it is simply passed through. If scopes is an
|
||||
iterable then a string is returned that is all the individual scopes
|
||||
concatenated with spaces.
|
||||
|
||||
Args:
|
||||
scopes: string or iterable of strings, the scopes.
|
||||
|
||||
Returns:
|
||||
The scopes formatted as a single string.
|
||||
"""
|
||||
if isinstance(scopes, six.string_types):
|
||||
return scopes
|
||||
else:
|
||||
return ' '.join(scopes)
|
||||
|
||||
|
||||
def dict_to_tuple_key(dictionary):
|
||||
"""Converts a dictionary to a tuple that can be used as an immutable key.
|
||||
|
||||
The resulting key is always sorted so that logically equivalent dictionaries
|
||||
always produce an identical tuple for a key.
|
||||
|
||||
Args:
|
||||
dictionary: the dictionary to use as the key.
|
||||
|
||||
Returns:
|
||||
A tuple representing the dictionary in it's naturally sorted ordering.
|
||||
"""
|
||||
return tuple(sorted(dictionary.items()))
|
||||
|
||||
|
||||
def _add_query_parameter(url, name, value):
|
||||
"""Adds a query parameter to a url.
|
||||
|
||||
Replaces the current value if it already exists in the URL.
|
||||
|
||||
Args:
|
||||
url: string, url to add the query parameter to.
|
||||
name: string, query parameter name.
|
||||
value: string, query parameter value.
|
||||
|
||||
Returns:
|
||||
Updated query parameter. Does not update the url if value is None.
|
||||
"""
|
||||
if value is None:
|
||||
return url
|
||||
else:
|
||||
parsed = list(urllib.parse.urlparse(url))
|
||||
q = dict(urllib.parse.parse_qsl(parsed[4]))
|
||||
q[name] = value
|
||||
parsed[4] = urllib.parse.urlencode(q)
|
||||
return urllib.parse.urlunparse(parsed)
|
||||
@@ -1,118 +0,0 @@
|
||||
#
|
||||
# Copyright 2014 the Melange authors.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""Helper methods for creating & verifying XSRF tokens."""
|
||||
|
||||
__authors__ = [
|
||||
'"Doug Coker" <dcoker@google.com>',
|
||||
'"Joe Gregorio" <jcgregorio@google.com>',
|
||||
]
|
||||
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import time
|
||||
|
||||
import six
|
||||
from oauth2client import util
|
||||
|
||||
|
||||
# Delimiter character
|
||||
DELIMITER = b':'
|
||||
|
||||
|
||||
# 1 hour in seconds
|
||||
DEFAULT_TIMEOUT_SECS = 1*60*60
|
||||
|
||||
|
||||
def _force_bytes(s):
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
s = str(s)
|
||||
if isinstance(s, six.text_type):
|
||||
return s.encode('utf-8')
|
||||
return s
|
||||
|
||||
|
||||
@util.positional(2)
|
||||
def generate_token(key, user_id, action_id="", when=None):
|
||||
"""Generates a URL-safe token for the given user, action, time tuple.
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
when: the time in seconds since the epoch at which the user was
|
||||
authorized for this action. If not set the current time is used.
|
||||
|
||||
Returns:
|
||||
A string XSRF protection token.
|
||||
"""
|
||||
when = _force_bytes(when or int(time.time()))
|
||||
digester = hmac.new(_force_bytes(key))
|
||||
digester.update(_force_bytes(user_id))
|
||||
digester.update(DELIMITER)
|
||||
digester.update(_force_bytes(action_id))
|
||||
digester.update(DELIMITER)
|
||||
digester.update(when)
|
||||
digest = digester.digest()
|
||||
|
||||
token = base64.urlsafe_b64encode(digest + DELIMITER + when)
|
||||
return token
|
||||
|
||||
|
||||
@util.positional(3)
|
||||
def validate_token(key, token, user_id, action_id="", current_time=None):
|
||||
"""Validates that the given token authorizes the user for the action.
|
||||
|
||||
Tokens are invalid if the time of issue is too old or if the token
|
||||
does not match what generateToken outputs (i.e. the token was forged).
|
||||
|
||||
Args:
|
||||
key: secret key to use.
|
||||
token: a string of the token generated by generateToken.
|
||||
user_id: the user ID of the authenticated user.
|
||||
action_id: a string identifier of the action they requested
|
||||
authorization for.
|
||||
|
||||
Returns:
|
||||
A boolean - True if the user is authorized for the action, False
|
||||
otherwise.
|
||||
"""
|
||||
if not token:
|
||||
return False
|
||||
try:
|
||||
decoded = base64.urlsafe_b64decode(token)
|
||||
token_time = int(decoded.split(DELIMITER)[-1])
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
if current_time is None:
|
||||
current_time = time.time()
|
||||
# If the token is too old it's not valid.
|
||||
if current_time - token_time > DEFAULT_TIMEOUT_SECS:
|
||||
return False
|
||||
|
||||
# The given token should match the generated one with the same time.
|
||||
expected_token = generate_token(key, user_id, action_id=action_id,
|
||||
when=token_time)
|
||||
if len(token) != len(expected_token):
|
||||
return False
|
||||
|
||||
# Perform constant time comparison to avoid timing attacks
|
||||
different = 0
|
||||
for x, y in zip(bytearray(token), bytearray(expected_token)):
|
||||
different |= x ^ y
|
||||
return not different
|
||||
@@ -1,3 +0,0 @@
|
||||
"""passlib - suite of password hashing & generation routinges"""
|
||||
|
||||
__version__ = '1.6.2'
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
17
setup-64.py
17
setup-64.py
@@ -1,17 +0,0 @@
|
||||
from distutils.core import setup
|
||||
import py2exe, sys, os
|
||||
|
||||
sys.argv.append('py2exe')
|
||||
|
||||
setup(
|
||||
console = ['gam.py'],
|
||||
|
||||
zipfile = None,
|
||||
options = {'py2exe':
|
||||
{'optimize': 2,
|
||||
'bundle_files': 3,
|
||||
'includes': ['passlib.handlers.sha2_crypt'],
|
||||
'dist_dir' : 'gam-64',
|
||||
'compressed' : True}
|
||||
}
|
||||
)
|
||||
16
setup.py
16
setup.py
@@ -1,16 +0,0 @@
|
||||
from distutils.core import setup
|
||||
import py2exe, sys, os
|
||||
|
||||
sys.argv.append('py2exe')
|
||||
|
||||
setup(
|
||||
console = ['gam.py'],
|
||||
|
||||
zipfile = None,
|
||||
options = {'py2exe':
|
||||
{'optimize': 2,
|
||||
'bundle_files': 3,
|
||||
'includes': ['passlib.handlers.sha2_crypt'],
|
||||
'dist_dir' : 'gam'}
|
||||
}
|
||||
)
|
||||
1
.gitignore → src/.gitignore
vendored
1
.gitignore → src/.gitignore
vendored
@@ -66,5 +66,4 @@ noverifyssl.txt
|
||||
gamcache/
|
||||
gam/
|
||||
gam-64/
|
||||
gam.spec
|
||||
*.zip
|
||||
809
src/GamCommands.txt
Normal file
809
src/GamCommands.txt
Normal file
@@ -0,0 +1,809 @@
|
||||
This document describes the GAM command line syntax in modified BNF, see https://en.wikipedia.org/wiki/Backus-Naur_Form
|
||||
Items on the command line are space separated, when an actual space character is required, it will be indicated by <Space>.
|
||||
If an item contains spaces, it should be surrounded by " or '.
|
||||
|
||||
[] optional item
|
||||
() group items
|
||||
* item may appear zero or more times
|
||||
+ item may appear one or more times
|
||||
| separates alternative items
|
||||
|
||||
# Primatives
|
||||
<Digit> ::= 0|1|2|3|4|5|6|7|8|9
|
||||
<Number> ::= <Digit>+
|
||||
<Hex> ::= <Digit>|a|b|c|d|e|f|A|B|C|D|E|F
|
||||
<Space> ::= an actual space character
|
||||
<String> ::= a string of characters, surrounded by " or ' if it contains spaces
|
||||
<TrueValues> ::= true|on|yes|enabled|1
|
||||
<FalseValues>= false|off|no|disabled|0
|
||||
<DataTransferService> ::= googleplus|google+|gplus|g+|googledrive|gdrive
|
||||
<ProductID> ::= Google-Apps|Google-Coordinate|Google-Drive-storage|Google-Vault
|
||||
<SKUID> ::= apps|gafb|gafw|gams|gau|unlimited|d4w|dfw|coordinate|vault|vfe|
|
||||
drive-20gb|drive20gb|20gb|drive-50gb|drive50gb|50gb|drive-200gb|drive200gb|200gb|drive-400gb|drive400gb|400gb|
|
||||
drive-1tb|drive1tb|1tb|drive-2tb|drive2tb|2tb|drive-4tb|drive4tb|4tb|drive-8tb|drive8tb|8tb|drive-16tb|drive16tb|16tb
|
||||
<Charset> ::= ascii|mbcs|utf-8|utf-8-sig|utf-16|<String>
|
||||
<FileFormat> ::= csv|html|txt|tsv|jpeg|jpg|png|svg|pdf|rtf|pptx|xlsx|docx|odt|ods|openoffice|ms|microsoft|micro$oft
|
||||
<Language> ::= ar|bn|bg|ca|zh-CN|zh-TW|hr|cs|da|nl|en|en-GB|et|fi|fr|de|el|gu|iw|is|in|it|ja|kn|ko|lv|lt|ms|ml|mr|no|or|fa|pl|pt-BR|pt-PT|ro|ru|sr|sk|sl|es|sv|tl|ta|te|th|tr|uk|ur|vi
|
||||
|
||||
# Basic items built from primatives
|
||||
<Boolean> ::= <TrueValues>|<FalseValues>
|
||||
<ByteCount> ::= <Number>[M|K|B]
|
||||
<CIDRnetmask> ::= <Number>.<Number>.<Number>.<Number>/<Number>
|
||||
<ColorHex> ::= #<Hex><Hex><Hex><Hex><Hex><Hex>
|
||||
<DomainName> ::= <String>(.<String>)+
|
||||
<EmailAddress> ::= <String>@<DomainName>
|
||||
<Year> ::= <Digit><Digit><Digit><Digit>
|
||||
<Month> ::= <Digit><Digit>
|
||||
<Day> ::= <Digit><Digit>
|
||||
<Hour> ::= <Digit><Digit>
|
||||
<Minute> ::= <Digit><Digit>
|
||||
<Second> ::= <Digit><Digit>
|
||||
<MilliSeconds> ::= <Digit><Digit><Digit>
|
||||
<Date> ::= <Year>-<Month>-<Day>
|
||||
<DateTime> ::= <Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>
|
||||
<Time> ::= <Year>-<Month>-<Day>(<Space>|T)<Hour>:<Minute>[:<Second>[.<MilliSeconds>[Z]]]
|
||||
<RegularExpression> ::= <Python Regular Expression, see: https://docs.python.org/2/library/re.html>
|
||||
<Tag> ::= <String>
|
||||
<UniqueID> ::= uid:<String>
|
||||
|
||||
# Named items
|
||||
<AccessToken> ::= <String>
|
||||
<ACLScope> ::= [user:]<EmailAddress>|group:<EmailAddress>|domain[:<DomainName>]|default
|
||||
<CalendarACLRole> ::= editor|freebusy|freebusyreader|owner|reader|writer
|
||||
<CalendarColorIndex> ::== <Number in range 1-24>
|
||||
<CalendarItem> ::= <EmailAddress>|<String>
|
||||
<ClientID> ::= <String>
|
||||
<CourseAlias> ::= <String>
|
||||
<CourseID> ::= <Number>|d:<CourseAlias>
|
||||
<CourseParticipantType> ::= teacher|teachers|student|students
|
||||
<CrOSID> ::= <String>
|
||||
<CrOSItem> ::= <CrOSID>|(query:<QueryCrOS>)|(query <QueryCrOS>)
|
||||
<DomainAlias> ::= <String>
|
||||
<DriveFileACLRole> :: =commenter|editor|owner|reader|writer
|
||||
<EventColorIndex> ::== <Number in range 1-11>
|
||||
<FieldName> ::= <String>
|
||||
<DestEmailAddress> ::= <EmailAddress>
|
||||
<DriveFileID> ::= <String>
|
||||
<DriveFileURL> :: = https://docs.google.com/a/<DomainName>/document/d/<DriveFileID>/<String>
|
||||
<DriveFileItem> ::= <DriveFileID>|<DriveFileURL>
|
||||
<DriveFileName> ::= <String>
|
||||
<DriveFolderID> ::= <String>
|
||||
<DriveFolderName> ::= <String>
|
||||
<EmailItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<EventID> ::= <String>
|
||||
<FieldName> ::= <String>
|
||||
<FileName> ::= <String>
|
||||
<FileNamePattern> ::= <String>
|
||||
<FilterID> ::= <Sttring>
|
||||
<FolderName> ::= <String>
|
||||
<GroupItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
<HostName> ::= <String>
|
||||
<Key> ::= <String>
|
||||
<LabelID> ::= <String>
|
||||
<LabelName> ::= <String>
|
||||
<LabelReplacement> ::= <String>
|
||||
<Marker> ::= <String>
|
||||
<MobileID> ::= <String>
|
||||
<MobileItem> ::= <MobileID>|(query:<QueryMobile>)|(query <QueryMobile>)
|
||||
<Name> ::= <String>
|
||||
<NotificationID> ::= <String>
|
||||
<OrgUnitID> ::= <String>
|
||||
<OrgUnitPath> ::= /|(/<String)+
|
||||
<PermissionID> ::= id:<String>|<EmailAddress>|anyone|anyonewithlink
|
||||
<PrinterID> ::= <String>
|
||||
<PrintJobAge> ::= <Number>(m|h|d)
|
||||
<PrintJobID> ::= <String>
|
||||
<PrintJobStatus> ::= done|error|held|in_progress|queued|submitted
|
||||
<PropertyKey> ::= <String>
|
||||
<PropertyValue> ::= <String>
|
||||
<QueryContact> ::= <String> See: https://developers.google.com/google-apps/contacts/v3/reference#contacts-query-parameters-reference
|
||||
<QueryCrOS> ::= <String> See: https://support.google.com/chrome/a/answer/1698333?hl=en
|
||||
<QueryDriveFile> :: = <String> See: https://developers.google.com/drive/v2/web/search-parameters
|
||||
<QueryGmail> ::= <String> See: https://support.google.com/mail/answer/7190
|
||||
<QueryMobile> ::= <String> See: https://support.google.com/a/answer/1408863?hl=en#search
|
||||
<QueryPrinter> ::= <String> See: https://developers.google.com/cloud-print/docs/appInterfaces#search
|
||||
<QueryPrintJob> ::= <String> See: https://developers.google.com/cloud-print/docs/appInterfaces#parameters_3
|
||||
<QueryUser> :: = <String> See: https://developers.google.com/admin-sdk/directory/v1/guides/search-users
|
||||
<RequestID> ::= <String>
|
||||
<ResourceID> ::= <String>
|
||||
<RoleItem> ::= <UniqueID>|<String>
|
||||
<RoleAssignmentID> ::= <String>
|
||||
<SchemaName> ::= <String>
|
||||
<Section> ::= <String>
|
||||
<StudentID> ::= <String>
|
||||
<TeacherID> ::= <String>
|
||||
<Timezone> ::= <String>
|
||||
<Title> ::= <String>
|
||||
<URI> ::= <String>
|
||||
<URL> ::= <String>
|
||||
<UserItem> ::= <EmailAddress>|<UniqueID>|<String>
|
||||
|
||||
|
||||
<CrOSFieldName> ::=
|
||||
activetimeranges|timeranges|
|
||||
annotatedassetid|assedid|asset|
|
||||
annotatedlocation|location|
|
||||
annotateduser|user|
|
||||
bootmode|
|
||||
deviceid|
|
||||
ethernetmacaddress|
|
||||
firmwareversion|
|
||||
lastenrollmenttime|
|
||||
lastsync|
|
||||
macaddress|
|
||||
meid|
|
||||
model|
|
||||
notes|
|
||||
ordernumber|
|
||||
orgunitpath|org|ou|
|
||||
osversion|
|
||||
platformversion|
|
||||
recentusers|
|
||||
serialnumber|
|
||||
status|
|
||||
supportenddate|
|
||||
willautorenew
|
||||
|
||||
<CrOSOrderByFieldName> ::=
|
||||
lastsync|location|notes|serialnumber|status|supportenddate|user
|
||||
|
||||
<DriveFieldName> ::=
|
||||
appdatacontents|
|
||||
cancomment|
|
||||
canreadrevisions|
|
||||
copyable|
|
||||
createddate|createdtime|
|
||||
description|
|
||||
editable|
|
||||
explicitlytrashed|
|
||||
fileextension|
|
||||
filesize|
|
||||
foldercolorrgb|
|
||||
fullfileextension|
|
||||
headrevisionid|
|
||||
iconlink|
|
||||
id|
|
||||
lastmodifyinguser|
|
||||
lastmodifyingusername|
|
||||
lastviewedbyme|lastviewedbymedate|lastviewedbymetime|lastviewedbyuser|
|
||||
md5|md5checksum|md5sum|
|
||||
mime|mimetype|
|
||||
modifiedbyme|modifiedbymedate|modifiedbymetime|modifiedbyuser|
|
||||
modifieddate|modifiedtime|
|
||||
name|
|
||||
originalfilename|
|
||||
ownedbyme|
|
||||
ownernames|
|
||||
owners|
|
||||
parents|
|
||||
permissions|
|
||||
quotabytesused|quotaused|
|
||||
restricted|
|
||||
shareable|
|
||||
shared|
|
||||
sharedwithmedate|sharedwithmetime|
|
||||
sharinguser|
|
||||
size|
|
||||
spaces|
|
||||
starred|
|
||||
thumbnaillink|
|
||||
title|
|
||||
trashed
|
||||
userpermission|
|
||||
version|
|
||||
viewed|
|
||||
viewerscancopycontent|
|
||||
webcontentlink|
|
||||
webviewlink|
|
||||
writerscanshare
|
||||
|
||||
|
||||
<GroupFieldName> ::=
|
||||
admincreated|
|
||||
aliases|
|
||||
description|
|
||||
email|
|
||||
id|
|
||||
name
|
||||
|
||||
<GroupSettingsFieldName> ::=
|
||||
allowexternalmembers|
|
||||
allowgooglecommunication|
|
||||
allowwebposting|
|
||||
archiveonly|
|
||||
customreplyto|
|
||||
defaultmessagedenynotificationtext|
|
||||
includeinglobaladdresslist|gal|
|
||||
isarchived|
|
||||
maxmessagebytes|
|
||||
memberscanpostasthegroup|
|
||||
messagedisplayfont|
|
||||
messagemoderationlevel|
|
||||
primarylanguage|
|
||||
replyto|
|
||||
sendmessagedenynotification|
|
||||
showingroupdirectory|
|
||||
spammoderationlevel|
|
||||
whocanadd|
|
||||
whocancontactowner|
|
||||
whocaninvite|
|
||||
whocanjoin|
|
||||
whocanleavegroup|
|
||||
whocanpostmessage|
|
||||
whocanviewgroup|
|
||||
whocanviewmembership
|
||||
|
||||
<MembersFieldName> ::=
|
||||
email|
|
||||
group|
|
||||
id|
|
||||
name|
|
||||
role|
|
||||
type
|
||||
|
||||
<MobileOrderByFieldName> ::=
|
||||
deviceid|email|lastsync|model|name|os|status|type
|
||||
|
||||
<OrgUnitFieldName> ::=
|
||||
description|id|inherit|name|orgunitpath|parent|parentid|inherit
|
||||
|
||||
<PrintJobOrderByFieldName> ::=
|
||||
create_time|status|title
|
||||
|
||||
<UserFieldName> ::=
|
||||
addresses|address|
|
||||
agreedtoterms|agreed2terms|
|
||||
changepasswordatnextlogin|changepassword|
|
||||
creationtime|
|
||||
deletiontime|
|
||||
email|emails|otheremail|otheremails|
|
||||
externalids|externalid|
|
||||
familyname|firstname|fullname|givenname|lastname|name|
|
||||
id|
|
||||
ims|im|
|
||||
includeinglobaladdresslist|gal|
|
||||
ipwhitelisted|
|
||||
isdelegatedadmin|admin|isadmin|
|
||||
ismailboxsetup|
|
||||
lastlogintime|
|
||||
noneditablealiases|aliases|nicknames|
|
||||
notes|note|
|
||||
organizations|organization|
|
||||
orgunitpath|org|ou|
|
||||
phones|phone|
|
||||
primaryemail|username|
|
||||
relations|relation|
|
||||
suspended|
|
||||
thumbnailphotourl|photo|photourl|
|
||||
websites|website|
|
||||
custom all|<SchemaNameList>
|
||||
|
||||
<UserOrderByFieldName> ::=
|
||||
familyname|lastname|givenname|firstname|email
|
||||
|
||||
# Named Lists
|
||||
# Lists can be in the following formats
|
||||
# Items, separated by commas, without spaces or commas in the items themselves: item(,item)*
|
||||
# Items, separated by spaces, without spaces or commas in the items themselves: "item( item)*"
|
||||
# Items, separated by commas, with spaces or commas in the items themselves: "'it em'(,'it em')*"
|
||||
# Items, separated by spaces, with spaces or commas in the items themselves: "'it em'( 'it em')*"
|
||||
|
||||
<ACLList> ::== '<ACLScope>(,<ACLScope>)*'
|
||||
<CalendarList> ::= '<CalendarItem>(,<CalendarItem>)*'
|
||||
<CourseAliasList> ::= '<CourseAlias>(,<CourseAlias>)*'
|
||||
<CourseIDList> ::= '<CourseID>(,<CourseID>)*'
|
||||
<CrOSFieldNameList> ::= '<CrOSFieldName(,<CrOSFieldName>)*'
|
||||
<CrOSList> ::= '<CrOSID>(,<CrOSID>)*'
|
||||
<DriveFileList> ::= '<DriveFileItem>(,<DriveFileItem>)*'
|
||||
<EmailAddressList> ::= '<EmailAddress>(,<EmailAddress>)*'
|
||||
<EventIDList> ::= '<EventID>(,<EventID>)*'
|
||||
<FileFormatList> ::= '<FileFormat>(,<FileFormat)*'
|
||||
<FilterIDList> ::= '<FilterID>(,<FilterID>)*'
|
||||
<GroupFieldNameList> ::= '<GroupFieldName(,<GroupFieldName>)*'
|
||||
<GroupSettingsFieldNameList> ::= '<GroupSettingsFieldName(,<GroupSettingsFieldName>)*'
|
||||
<GroupList> ::= '<GroupItem>(,<GroupItem>)*'
|
||||
<LabelNameList> ::= '<LabelName>(,<LabelName)*'
|
||||
<MembersFieldNameList> ::= '<MembersFieldName(,<MembersFieldName>)*'
|
||||
<MobileList> ::= '<MobileId>(,<MobileId>)*'
|
||||
<OrgUnitList> ::== '<OrgUnitPath>(,<OrgUnitPath>)*'
|
||||
<PrinterIDList> ::= '<PrinterID>(,<PrinterID>)*'
|
||||
<ProductIDList> ::= '(<ProductID>|SKUID>)(,<ProductID>|SKUID>)*'
|
||||
<PrintJobIDList> ::= '<PrintJobID>(,<PrintJobID>)*'
|
||||
<ResourceIDList> ::= '<ResourceID>(,<ResourceID>)*'
|
||||
<SKUIDList> ='<SKUID>(,<SKUID>)*'
|
||||
<SchemaNameList> ::= '<SchemaName>(,<SchemaName>)*'
|
||||
<UserFieldNameList> ::= '<UserFieldName(,<UserFieldName>)*'
|
||||
<UserList> ::= '<UserItem>(,<UserItem>)*'
|
||||
|
||||
# Specify a collection of ChromeOS devices by directly specifying them
|
||||
<CrOSTypeEntity> ::=
|
||||
(all cros)|
|
||||
(cros <CrOSList>)|
|
||||
# Specify a collection of Users by directly specifying them or by specifiying items that will yield a list of users
|
||||
<UserTypeEntity> ::=
|
||||
(all users)|
|
||||
(user <UserItem>)|
|
||||
(users <UserList>)|
|
||||
(group <GroupItem)|
|
||||
(ou|org <OrgUnitPath)|
|
||||
(ou_and_children|ou_and_child <OrgUnitPath>)|
|
||||
(courseparticipants <CourseID>)|
|
||||
(students <CourseID>)|
|
||||
(teachers <CourseID>)|
|
||||
(file <FileName>)|
|
||||
(csvfile <FileName>:<FieldName>)|
|
||||
(license|licenses|licence|licences <SKUIDList>)|
|
||||
(query <QueryUser>)
|
||||
|
||||
|
||||
# Item attributes
|
||||
<CalendarAttributes> ::=
|
||||
(selected <Boolean>)|(hidden <Boolean>)|(summary <String>)|(colorindex|colorid <CalendarColorIndex>)|(backgroundcolor <ColorHex>)|(foregroundcolor <ColorHex>)|
|
||||
(reminder clear|(email|sms|pop <Number>))|
|
||||
(notification clear|(email|sms eventcreation|eventchange|eventcancellation|eventresponse|agenda))
|
||||
|
||||
<CalendarSettings> ::==
|
||||
(summary <String>)|(description <String>)|(location <String>)|(timezone <String>)
|
||||
|
||||
<CourseAttributes> ::=
|
||||
(name <String>)|(section <string>)|(heading <String>)|(room <String>)|(description <String>)|
|
||||
(state|status active|archived|provisioned|declined)
|
||||
|
||||
<CrOSAttributes> ::=
|
||||
(asset|assetid|tag <String>)|
|
||||
(location <String>)|
|
||||
(notes <String>)|
|
||||
(org|ou <OrgUnitPath>)|
|
||||
(status active|deprovisioned|inactive|returnapproved|returnrequested|shipped|unknown)|
|
||||
(user <Name>)
|
||||
|
||||
<DriveFileAddAttributes> ::=
|
||||
(localfile <FileName>)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|(restricted|restrict)|(starred|star)|(trashed|trash)|(viewed|view)|
|
||||
(lastviewedbyme <Time>)|(modifieddate|modifiedtime <Time>)|(description <String>)|(mimetype gdoc|gdocument|gdrawing|gfolder|gdirectory|gform|gfusion|gpresentation|gscript|gsite|gsheet|gspreadsheet)|
|
||||
(parentid <DriveFolderID>)|(parentname <FolderName>)|writerscantshare
|
||||
<DriveFileUpdateAttributes> ::=
|
||||
(localfile <FileName>)|
|
||||
(convert)|(ocr)|(ocrlanguage <Language>)|(restricted|restrict <Boolean>)|(starred|star <Boolean>)|(trashed|trash <Boolean>)|(viewed|view <Boolean>)|
|
||||
(lastviewedbyme <Time>)|(modifieddate <Time>)|(description <String>)|(mimetype gdoc|gdocument|gdrawing|gfolder|gdirectory|gform|gfusion|gpresentation|gscript|gsite|gsheet|gspreadsheet)|
|
||||
(parentid <DriveFolderID>)|(parentname <FolderName>)|writerscantshare
|
||||
<EventAttributes> ::=
|
||||
(anyonecanaddself)|(guestscantinviteothers)|(guestscantseeothers)|(notifyattendees)|(available)|(visibility default|public|prvate)|(tentative)|
|
||||
(attendee <EmailAddress>)|(optionalattendee <EmailAddress>)|
|
||||
(description <String>)|(summary <String>)|(location <String>)|(id <String>)|
|
||||
(source <String> <URL>)|(privateproperty <PropertyKey> <PropertyValue>)|(sharedproperty <PropertyKey> <PropertyValue>)|
|
||||
(recurrence <RRULE, EXRULE, RDATE and EXDATE line>)|
|
||||
(start allday <Date>)|(start <Time>)|(end allday <Date>)|(end <Time>)|(timezone <Timezone>)|
|
||||
(noreminders|(reminder email|popup|sms <Number>))|
|
||||
(colorindex|colorid <EventColorIndex>)
|
||||
|
||||
<GroupAttributes> ::=
|
||||
(allowexternalmembers <Boolean>)|
|
||||
(allowgooglecommunication <Boolean>)|
|
||||
(allowwebposting <Boolean>)|
|
||||
(archiveonly <Boolean>)|
|
||||
(customreplyto <EmailAddress>)|
|
||||
(defaultmessagedenynotificationtext <String>)|
|
||||
(description <String>)|
|
||||
(gal|includeInGlobalAddressList <Boolean>)|
|
||||
(isarchived <Boolean>)|
|
||||
(maxmessagebytes <ByteCount>)|
|
||||
(memberscanpostasthegroup <Boolean>)|
|
||||
(messagedisplayfont DEFAULT_FONT|FIXED_WIDTH_FONT)|
|
||||
(messagemoderationlevel MODERATE_ALL_MESSAGES|MODERATE_NON_MEMBERS|MODERATE_NEW_MEMBERS|MODERATE_NONE)|
|
||||
(name <String>)|
|
||||
(primarylanguage <Language>)|
|
||||
(replyto REPLY_TO_CUSTOM|REPLY_TO_SENDER|REPLY_TO_LIST|REPLY_TO_OWNER|REPLY_TO_IGNORE|REPLY_TO_MANAGERS)|
|
||||
(sendmessagedenynotification <Boolean>)|
|
||||
(showingroupdirectory <Boolean>)|
|
||||
(spammoderationlevel ALLOW|MODERATE|SILENTLY_MODERATE|REJECT)|
|
||||
(whocanadd ALL_MEMBERS_CAN_ADD|ALL_MANAGERS_CAN_ADD|NONE_CAN_ADD)|
|
||||
(whocancontactowner ANYONE_CAN_CONTACT|ALL_IN_DOMAIN_CAN_CONTACT|ALL_MEMBERS_CAN_CONTACT|ALL_MANAGERS_CAN_CONTACT)|
|
||||
(whocaninvite ALL_MEMBERS_CAN_INVITE|ALL_MANAGERS_CAN_INVITE|NONE_CAN_INVITE)|
|
||||
(whocanjoin ANYONE_CAN_JOIN|ALL_IN_DOMAIN_CAN_JOIN|INVITED_CAN_JOIN|CAN_REQUEST_TO_JOIN)|
|
||||
(whocanleavegroup ALL_MANAGERS_CAN_LEAVE|ALL_MEMBERS_CAN_LEAVE|NONE_CAN_LEAVE)|
|
||||
(whocanpostmessage NONE_CAN_POST|ALL_MANAGERS_CAN_POST|ALL_MEMBERS_CAN_POST|ALL_IN_DOMAIN_CAN_POST|ANYONE_CAN_POST)|
|
||||
(whocanviewgroup ANYONE_CAN_VIEW|ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)|
|
||||
(whocanviewmembership ALL_IN_DOMAIN_CAN_VIEW|ALL_MEMBERS_CAN_VIEW|ALL_MANAGERS_CAN_VIEW)
|
||||
|
||||
<MobileAttributes> ::=
|
||||
(model <String>)|(os <String>)|(useragent <String>)|
|
||||
(action admin_remote_wipe|wipe|admin_account_wipe|accountwipe|wipeaccount|approve|block|cancel_remote_wipe_then_activate|cancel_remote_wipe_then_block)
|
||||
|
||||
<PrinterAttributes> ::= (currentquota <Number>)|(dailyquota <Number>)|
|
||||
(defaultdisplayname <String>)|(description <String>)|(displayname <String>)|(firmware <String>)|(gcpversion <String>)|
|
||||
(istosaccepted <Boolean>)|(manufacturer <String>)|(model <String>)|(name <String>)|(ownerid <EmailAddress>)|(proxy <String>)|(public <Boolean>)|
|
||||
(quotaenabled <Boolean>)|(status <Number>)|(type <String>)|(uuid <String>)|
|
||||
(setupurl <URL>)|(supporturl <URL>)|(updateurl <URL>)
|
||||
|
||||
<SchemaFieldDefinition> ::=
|
||||
field <FieldName> (type bool|date|double|email|int64|phone|string) [multivalued|multivalue] [indexed] [restricted] [range <Number> <Number>] endfield
|
||||
|
||||
<UserAttributes> ::=
|
||||
(address|addresses clear|(type work|home|other|(custom <String>) [unstructured|formatted <String>] [pobox <String>] [extendedaddress <String>] [streetaddress <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country <String>] [countrycode <String>] notprimary|primary))|
|
||||
(admin <Boolean>)|
|
||||
(agreed2terms|agreedtoterms <Boolean>)|
|
||||
(changepassword|changepasswordatnextlogin <Boolean>)|
|
||||
(crypt|sha|sha1|sha-1|md5|nohash)|
|
||||
(customerid <String>)|
|
||||
(email|primaryemail|username <EmailAddress>)|
|
||||
(emails|otheremail|otheremails clear|(work|home|other|<String> <String>))|
|
||||
(externalid|externalids clear|(account|customer|network|organization|<String> <String>))|
|
||||
(firstname|givenname <String>)|
|
||||
(gal|includeinglobaladdresslist <Boolean>)|
|
||||
(im|ims clear|(type work|home|other|(custom <String>) protocol aim|gtalk|icq|jabber|msn|net_meeting|qq|skype|yahoo|(custom_protocol <String>) <String> [notprimary|primary]))|
|
||||
(ipwhitelisted <Boolean>)|
|
||||
(lastname|familyname <String>)|
|
||||
(note|notes clear|([text_plain|text_html] <String>|(file <FileName>)))|
|
||||
(organization|organizations clear|([type domain_only|school|unknown|work] [customtype <String>] [name <String>] [title <String>] [department <String>] [symbol <String>]
|
||||
[costcenter <String>] [location <String>] [description <String>] [domain <String>] notprimary|primary))|
|
||||
(org|ou|orgunitpath <OrgUnitPath>)
|
||||
(password random|<String>)|
|
||||
(phone|phones clear|([type work|home|other|work_fax|home_fax|other_fax|main|company_main|assistant|mobile|work_mobile|pager|work_pager|car|radio|callback|isdn|telex|tty_tdd|grand_central|(custom <String>)
|
||||
[value <String>] notprimary|primary))|
|
||||
(relation|relations clear|(spouse|child|mother|father|parent|brother|sister|friend|relative|domestic_partner|manager|assistant|referred_by|partner|<String> <String>))|
|
||||
(suspended <Boolean>)|
|
||||
(website|websites clear|(home_page|blog|profile|work|home|other|ftp|reservations|app_install_page|<String> <URL> [notprimary|primary]))|
|
||||
(<SchemaName>.<FieldName> [multivalued|multivalue|value] <String>)
|
||||
|
||||
|
||||
gam version
|
||||
gam help
|
||||
|
||||
gam batch <FileName>|- [charset <Charset>]
|
||||
gam csv <FileName>|- [charset <Charset>] [matchfield <FieldName> <RegularExpression>] gam <GAM argument list>
|
||||
|
||||
gam oauth|oauth2 create|request
|
||||
gam oauth|oauth2 delete|revoke
|
||||
gam oauth|oauth2 info|verify [<AccessToken>]
|
||||
|
||||
gam whatis <EmailItem>
|
||||
|
||||
gam report users|user [todrive]
|
||||
[date <Date>] [(user all|<UserItem>)] [filter|filters <String>] [fields|parameters <String>]
|
||||
gam report customers|customer|domain [todrive]
|
||||
[date <Date>] [fields|parameters <String>]
|
||||
gam report admin|calendar|calendars|drive|docs|doc|groups|group|logins|login|mobile|tokens|token [todrive]
|
||||
[start <Time>] [end <Time>] [(user all|<UserItem>)] [event <String>] [filter|filters <String>] [ip <String>]
|
||||
|
||||
gam create admin <UserItem> <RoleItem> customer|(org_unit <OrgUnitItem>)
|
||||
gam delete admin <RoleAssignmentId>
|
||||
gam print admins [todrive] [user <UserItem>] [role <RoleItem>]
|
||||
gam print adminroles|roles [todrive]
|
||||
|
||||
gam create domain <DomainName>
|
||||
gam update domain <DomainName> primary
|
||||
gam delete domain <DomainName>
|
||||
gam info domain [<DomainName>]
|
||||
gam print domains [todrive]
|
||||
|
||||
gam create domainalias|aliasdomain <DomainAlias> <DomainName>
|
||||
gam delete domainalias|aliasdomain <DomainAlias>
|
||||
gam info domainalias|aliasdomain <DomainAlias>
|
||||
gam print domainaliases|aliasdomains [todrive]
|
||||
|
||||
|
||||
gam info customer
|
||||
gam update customer [adminsecondaryemail|alternateemail <EmailAddress>] [language <LanguageCode] [phone|phonenumber <String>]
|
||||
[contact|contactname <String>] [name|organizationname <String>]
|
||||
[address1|addressline1 <String>] [address2|addressline2 <String>] [address3|addressline3 <String>]
|
||||
[locality <String>] [region <String>] [postalcode <String>] [country|countrycode <String>]
|
||||
|
||||
gam create datatransfer|transfer <OldOwnerID> <DataTransferService> <NewOwnerID>
|
||||
gam info datatransfer|transfer <TransferID>
|
||||
gam print datatransfers|transfers [todrive] [olduser|oldowner <UserItem>] [newuser|newowner <UserItem>] [status <String>]
|
||||
|
||||
gam print transferapps
|
||||
|
||||
gam update instance logo <FileName>
|
||||
gam update instance sso_settings [enabled <Boolean>] [sign_on_uri <URI>] [sign_out_uri <URI>] [password_uri <URI>] [whitelist <CIDRnetmask>] [use_domain_specific_issuer <Boolean>]
|
||||
gam update instance sso_key <FileName>
|
||||
gam info instance [logo <FileName>]
|
||||
|
||||
gam create org|ou <Name> [description <String>] [parent <OrgUnitPath>] [inherit|noinherit]
|
||||
gam update org|ou <OrgUnitPath> [name <Name>] [description <String>] [parent <OrgUnitPath>] [inherit|noinherit]
|
||||
gam update org|ou <OrgUnitPath> add|move <CrOSTypeEntity>|<UserTypeEntity>
|
||||
gam delete org|ou <OrgUnitPath>
|
||||
gam info org|ou <OrgUnitPath> [nousers] [children|child]
|
||||
gam print orgs|ous [todrive] [toplevelonly] [from_parent <OrgUnitPath>] [allfields|<OrgUnitFieldName>*]
|
||||
|
||||
gam create alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
|
||||
gam update alias|nickname <EmailAddress> user|group|target <UniqueID>|<EmailAddress>
|
||||
gam delete alias|nickname [user|group|target] <UniqueID>|<EmailAddress>
|
||||
gam info alias|nickname <EmailAddress>
|
||||
gam print aliases|nicknames [todrive]
|
||||
|
||||
gam audit uploadkey <ValueReadFromStdin>
|
||||
gam audit activity request <EmailAddress>
|
||||
gam audit activity delete <EmailAddress> <RequestID>
|
||||
gam audit activity download <EmailAddress> <RequestID>
|
||||
gam audit activity status [<EmailAddress> <RequestID>]
|
||||
|
||||
gam audit export request <EmailAddress> [begin <DateTime>] [end <DateTime>] [search <QueryGmail>] [headersonly] [includedeleted]
|
||||
gam audit export delete <EmailAddress> <RequestID>
|
||||
gam audit export download <EmailAddress> <RequestID>
|
||||
gam audit export status [<EmailAddress> <RequestID>]
|
||||
gam audit export watch <EmailAddress> <RequestID>
|
||||
|
||||
gam audit monitor create <EmailAddress> <DestEmailAddress> [begin <DateTime>] [end <DateTime>] [incoming_headers] [outgoing_headers] [nochats] [nodrafts] [chat_headers] [draft_headers]
|
||||
gam audit monitor delete <EmailAddress> <DestEmailAddress>
|
||||
gam audit monitor list <EmailAddress>
|
||||
|
||||
gam calendar <CalendarItem> add <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> update <CalendarACLRole> ([user] <EmailAddress>)|(group <EmailAddress>)|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> del|delete <CalendarACLRole> <EmailAddress>|(domain [<DomainName>])|default
|
||||
gam calendar <CalendarItem> showacl
|
||||
|
||||
gam calendar <CalendarItem> addevent <EventAttributes>+
|
||||
gam calendar <CalendarItem> wipe
|
||||
|
||||
gam update cros <CrOSItem> <CrOSAttributes>+
|
||||
gam info cros <CrOSItem> [nolists] [listlimit <Number>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
||||
|
||||
gam print cros [todrive] [query <QueryCrOS>]
|
||||
[orderby <CrOSOrderByFieldName> [ascending|descending]] [nolists] [listlimit <Number>]
|
||||
[basic|full|allfields] <CrOSFieldName>* [fields <CrOSFieldNameList>]
|
||||
gam <CrOSTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
gam print cros
|
||||
Prints a header row and deviceId for all CrOS devices.
|
||||
gam <CrOSTypeEntity> print cros
|
||||
Prints no header row and deviceId for specified CrOS devices.
|
||||
gam print cros ... basic|full
|
||||
Prints a header row and selected fields for specified CrOS devices.
|
||||
|
||||
The basic argument yields these column headers: deviceId,annotatedAssetId,annotatedLocation,annotatedUser,lastSync,notes,serialNumber,status
|
||||
The full argument yields all column headers including two headers, recentUsers and activeTimeRanges,
|
||||
that repeat with two subvalues each, yielding a large number of columns that make the output hard to process.
|
||||
The nolists argument suppresses these two headers; if you want these headers in a more manageable form use the following arguments.
|
||||
The listlimit <Number> argument limits the number of repetitions to <Number>. If <Number> equals zero, there is no limit.
|
||||
If recentusers is specified as a field, each pair of values for recentUsers is put on a separate row with all of the other headers.
|
||||
If timeranges is specified as a field, each pair of values for activeTimeRanges is put on a separate row with all of the other headers.
|
||||
|
||||
gam update mobile <MobileItem> <MobileAttributes>+
|
||||
gam delete mobile <MobileItem>
|
||||
gam info mobile <MobileItem>
|
||||
gam print mobile [todrive] [query <QueryMobile>] [basic|full] [orderby <MobileOrderByFieldName> [ascending|descending]]
|
||||
|
||||
gam create group <EmailAddress> <GroupAttributes>*
|
||||
gam update group <GroupItem> [admincreated <Boolean>] [email <EmailAddress>] <GroupAttributes>*
|
||||
gam update group <GroupItem> add [owner|manager|member] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> delete|remove [owner|manager|member] <UserTypeEntity>
|
||||
gam update group <GroupItem> sync [owner|manager|member] [notsuspended] <UserTypeEntity>
|
||||
gam update group <GroupItem> update [owner|manager|member] <UserTypeEntity>
|
||||
gam delete group <GroupItem>
|
||||
gam info group <GroupItem> [nousers] [noaliases] [groups]
|
||||
gam update group <GroupItem> clear [owner] [manager] [member]
|
||||
|
||||
gam print groups [todrive] ([domain <DomainName>] [member <UserItem>])
|
||||
[maxresults <Number>] [delimiter <String>]
|
||||
[members] [owners] [managers] [settings] <GroupFieldName>* [fields <GroupFieldNameList>|<GroupSettingsFieldNameList>]
|
||||
|
||||
|
||||
gam print group-members|groups-members [todrive] ([domain <DomainName>] [member <UserItem>])|[group <GroupItem>]
|
||||
[membernames] [fields <MembersFieldNameList>]
|
||||
|
||||
gam print license|licenses|licence|licences [todrive] [products|product <ProductIDList>] [skus|sku <SKUIDList>]
|
||||
|
||||
gam update notification|notifications [(id all)|(id <NotificationID>)*] unread|read
|
||||
gam delete notification|notifications [(id all)|(id <NotificationID>)*]
|
||||
gam info notification|notifications [unreadonly]
|
||||
|
||||
gam create resource <ResourceID> <Name> [description <String>] [type <String>]
|
||||
gam update resource <ResourceID> [name <Name>] [description <String>] [type <String>]
|
||||
gam delete resource <ResourceID>
|
||||
gam info resource <ResourceID>
|
||||
gam print resources [todrive] [allfields] [id] [name] [description] [email] [type]
|
||||
|
||||
gam create schema|schemas <SchemaName> <SchemaFieldDefinition>+
|
||||
gam update schema <SchemaName> <SchemaFieldDefinition>+
|
||||
gam delete schema <SchemaName>
|
||||
gam info schema <SchemaName>
|
||||
gam show schema|schemas
|
||||
gam print schema|schemas
|
||||
|
||||
gam create user <EmailAddress> <UserAttrubutes>*
|
||||
gam update user <UserItem> <UserAttributes>+
|
||||
gam delete user <UserItem>
|
||||
gam undelete user <UserItem> [org|ou <OrgUnitPath>]
|
||||
gam info user [<UserItem>] [noaliases] [nogroups] [nolicenses|nolicences] [noschemas] [schemas <SchemaNameList>] [userview] [skus|sku <SKUIDList>]
|
||||
|
||||
gam print users [todrive] ([domain <DomainName>] [query <QueryUser>] [deleted_only|only_deleted])
|
||||
[groups] [license|licenses|licence|licences] [emailpart|emailparts|username]
|
||||
[orderby <UserOrderByFieldName> [ascending|descending]] [userview]
|
||||
[basic|full|allfields | <UserFieldName>* | fields <UserFieldNameList>]
|
||||
gam <UserTypeEntity> print
|
||||
|
||||
Summary of printing:
|
||||
gam print users
|
||||
Prints a header row and primaryEmail for all users.
|
||||
gam <UserTypeEntity> print users
|
||||
Prints no header row and primaryEmail for specified users.
|
||||
|
||||
gam create verify|verification <DomainName>
|
||||
gam update verify|verification <DomainName> cname|txt|text|site|file
|
||||
gam info verify|verification
|
||||
|
||||
gam create course id|alias <CourseAlias> [teacher <UserItem>] <CourseAttributes>*
|
||||
gam update course <CourseID> <CourseAttributes>+
|
||||
gam delete course <CourseID>
|
||||
gam info course <CourseID>
|
||||
gam print courses [todrive] [teacher] [student] [alias|aliases] [delimiter <String>]
|
||||
|
||||
gam course <CourseID> add alias <CourseAlias>
|
||||
gam course <CourseID> delete alias <CourseAlias>
|
||||
gam course <CourseID> add teachers|students <UserItem>
|
||||
gam course <CourseID> delete|remove teachers|students <UserItem>
|
||||
gam course <CourseID> sync teachers|students <UserTypeEntity>
|
||||
gam print course-participants [todrive](course <CourseID>)* [teacher] [student] [show all|students|teachers]
|
||||
|
||||
gam printer register
|
||||
gam update printer <PrinterID> <PrinterAttributes>+
|
||||
gam delete printer <PrinterID>
|
||||
gam info printer <PrinterID> [everything]
|
||||
gam print printers [todrive] [query <QueryPrinter>] [type <String>] [status <String>] [extrafields <String>]
|
||||
|
||||
gam printer <PrinterID> add user|manager|owner <EmailAddress>|[domain:]<DomainName>|public
|
||||
gam printer <PrinterID> delete <EmailAddress>|[domain:]<DomainName>|public
|
||||
gam printer <PrinterID> showacl
|
||||
gam printjob <PrintJobID> cancel
|
||||
gam printjob <PrintJobID> delete
|
||||
gam printjob <PrintJobID> resubmit <PrinterID>
|
||||
|
||||
gam printjob <PrinterID>|any fetch
|
||||
[olderthan|newerthan <PrintJobAge>] [query <QueryPrintJob>]
|
||||
[status <PrintJobStatus>]
|
||||
[orderby <PrintJobOrderByFieldName> [ascending|descending]]
|
||||
[owner|user <EmailAddress>]
|
||||
[limit <Number>] [drivedir|(targetfolder <FilePath>)]
|
||||
gam printjob <PrinterID> submit <FileName>|<URL> [name|title <String>] (tag <String>)*
|
||||
gam print printjobs [todrive] [printer|printerid <PrinterID>]
|
||||
[olderthan|newerthan <PrintJobAge>] [query <QueryPrintJob>]
|
||||
[status <PrintJobStatus>]
|
||||
[orderby <PrintJobOrderByFieldName> [ascending|descending]]
|
||||
[owner|user <EmailAddress>]
|
||||
[limit <Number>]
|
||||
|
||||
gam <UserTypeEntity> delete|del asp|asps|applicationspecificpasswords <AspID>
|
||||
gam <UserTypeEntity> show asps|asp|applicationspecificpasswords
|
||||
|
||||
gam <UserTypeEntity> update backupcodes|backupcode|verificationcodes
|
||||
gam <UserTypeEntity> delete|del backupcodes|backupcode|verificationcodes
|
||||
gam <UserTypeEntity> show backupcodes|backupcode|verificationcodes
|
||||
|
||||
gam <UserTypeEntity> add calendar <CalendarItem> <CalendarAttributes>*
|
||||
gam <UserTypeEntity> update calendar <CalendarItem> <CalendarAttributes>+
|
||||
gam <UserTypeEntity> delete|del calendar <CalendarItem>
|
||||
gam <UserTypeEntity> show calendars
|
||||
gam <UserTypeEntity> info calendar <CalendarItem>|primary
|
||||
gam <UserTypeEntity> print calendars [todrive]
|
||||
|
||||
gam <UserTypeEntity> show calsettings
|
||||
gam <UserTypeEntity> update calattendees csv <FileName> [dryrun] [start <Date>] [end <Date>] [allevents]
|
||||
gam <UserTypeEntity> transfer seccals <UserItem> [keepuser]
|
||||
|
||||
gam <UserTypeEntity> print|show driveactivity [todrive] [fileid <DriveFileID>] [folderid <DriveFolderID>]
|
||||
gam <UserTypeEntity> print|show drivesettings [todrive]
|
||||
gam <UserTypeEntity> print|show filelist [todrive] [query <QueryDriveFile>] [allfields|<DriveFieldName>*]
|
||||
gam <UserTypeEntity> show fileinfo <DriveFileID> [allfields|<DriveFieldName>*]
|
||||
gam <UserTypeEntity> show filerevisions <DriveFileID>
|
||||
gam <UserTypeEntity> show filetree
|
||||
|
||||
gam <UserTypeEntity> add drivefile [drivefilename <DriveFileName>] <DriveFileAddAttributes>*
|
||||
gam <UserTypeEntity> update drivefile (id <DriveFileID)|(drivefilename <DriveFileName>)|(query <QueryDriveFile) [copy] [newfilename <DriveFileName>] <DriveFileUpdateAttributes>*
|
||||
gam <UserTypeEntity> get drivefile (id <DriveFileID>)|(drivefilename <DriveFileName>)|(query <QueryDriveFile>) [format <FileFormatList>] [targetfolder <FilePath>] [revision <Number>]
|
||||
gam <UserTypeEntity> delete|del drivefile <DriveFileID>|<DriveFileURL>|(query:<QueryDriveFile>) [purge|untrash]
|
||||
gam <UserTypeEntity> transfer drive <UserItem> [keepuser]
|
||||
gam <UserTypeEntity> delete|del emptydrivefolders
|
||||
gam <UserTypeEntity> empty drivetrash
|
||||
|
||||
|
||||
gam <UserTypeEntity> add drivefileacl <DriveFileID> anyone|(user <UserItem>)|(group <GroupItem>)|(domain <DomainName>)
|
||||
(role <DriveFileACLRole>) [withlink] [sendmail] [emailmessage <String>]
|
||||
gam <UserTypeEntity> update drivefileacl <DriveFileID> <PermissionID>
|
||||
(role <DriveFileACLRole>) [withlink] [transferownership <Boolean>]
|
||||
gam <UserTypeEntity> delete|del drivefileacl <DriveFileID> <PermissionID>
|
||||
gam <UserTypeEntity> show drivefileacl <DriveFileID>
|
||||
|
||||
gam <UserTypeEntity> delete|del alias|aliases
|
||||
|
||||
gam <UserTypeEntity> delete|del group|groups
|
||||
|
||||
gam <UserTypeEntity> add license <SKUID>
|
||||
gam <UserTypeEntity> update license <SKUID> [from] <SKUID>
|
||||
gam <UserTypeEntity> delete|del license <SKUID>
|
||||
|
||||
gam <UserTypeEntity> update photo <FileNamePattern>
|
||||
gam <UserTypeEntity> delete|del photo
|
||||
gam <UserTypeEntity> get photo [drivedir|(targetfolder <FilePath>)] [noshow]
|
||||
|
||||
gam <UserTypeEntity> profile share|shared|unshare|unshared
|
||||
gam <UserTypeEntity> show profile
|
||||
|
||||
gam <UserTypeEntity> delete|del token|tokens clientid <ClientID>
|
||||
gam <UserTypeEntity> show tokens|token [clientid <ClientID>]
|
||||
gam <UserTypeEntity> print tokens|token [todrive] [clientid <ClientID>]
|
||||
gam print tokens|token [todrive] [clientid <ClientID>] [<UserTypeEntity>]
|
||||
|
||||
gam <UserTypeEntity> update user <UserAttrubutes>
|
||||
|
||||
gam <UserTypeEntity> deprovision|deprov
|
||||
#
|
||||
# Update user Gmail mailbox
|
||||
#
|
||||
gam <UserTypeEntity> [add] label|labels <Name> [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
||||
gam <UserTypeEntity> update labelsettings <LabelName> [name <Name>] [messagelistvisibility hide|show] [labellistvisibility hide|show|showifunread]
|
||||
gam <UserTypeEntity> update label|labels [search <RegularExpression>] [replace <LabelReplacement>] [merge]
|
||||
gam <UserTypeEntity> delete|del label|labels <LabelName>|regex:<RegularExpression>|--ALL_LABELS--
|
||||
gam <UserTypeEntity> show labels|label [onlyuser] [showcounts]
|
||||
|
||||
gam <UserTypeEntity> delete messages query <QueryGmail> [doit] [max_to_delete|max_to_process <Number>]
|
||||
gam <UserTypeEntity> modify messages query <QueryGmail> [doit] [max_to_modify|max_to_process <Number>] (addlabel <LabelName>)* (removelabel <LabelName>)*
|
||||
gam <UserTypeEntity> trash messages query <QueryGmail> [doit] [max_to_trash|max_to_process <Number>]
|
||||
gam <UserTypeEntity> untrash messages query <QueryGmail> [doit] [max_to_untrash|max_to_process <Number>]
|
||||
#
|
||||
# Update user Gmail settings
|
||||
#
|
||||
gam <UserTypeEntity> show gmailprofile [todrive]
|
||||
gam <UserTypeEntity> show gplusprofile [todrive]
|
||||
gam <UserTypeEntity> arrows <Boolean>
|
||||
|
||||
gam <UserTypeEntity> add delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> delegate|delegates to <EmailAddress>
|
||||
gam <UserTypeEntity> delete|del delegate|delegates <EmailAddress>
|
||||
gam <UserTypeEntity> show delegates|delegate [csv]
|
||||
gam <UserTypeEntity> print delegates [todrive]
|
||||
|
||||
gam <UserTypeEntity> [add] filter [from <EmailAddress>] [to <EmailAddress>] [subject <String>] [haswords|query <List>] [nowords|negatedquery <List>] [musthaveattachment|hasattachment] [excludechats] [size larger|smaller <ByteCount>]
|
||||
[label <LabelID>] [important|notimportant] [star] [trash] [markread] [archive] [neverspam] [forward <EmailAddress>]
|
||||
gam <UserTypeEntity> delete filters <FilterIDEntity>
|
||||
gam <UserTypeEntity> show filters
|
||||
gam <UserTypeEntity> info filters <FilterIDEntity>
|
||||
gam <UserTypeEntity> print filters [todrive]
|
||||
|
||||
gam <UserTypeEntity> forward <FalseValues>
|
||||
gam <UserTypeEntity> forward <TrueValues> keep|leaveininbox|archive|delete|trash|markread <EmailAddress>
|
||||
gam <UserTypeEntity> show forward
|
||||
gam <UserTypeEntity> print forward [todrive]
|
||||
|
||||
gam <UserTypeEntity> add forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> delete forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> show forwardingaddress|forwardingaddresses
|
||||
gam <UserTypeEntity> info forwardingaddress|forwardingaddresses <EmailAddress>
|
||||
gam <UserTypeEntity> print forwardingaddress|forwardingaddresses [todrive]
|
||||
|
||||
gam <UserTypeEntity> imap|imap4 <Boolean> [noautoexpunge] [expungebehavior archive|deleteforever|trash] [maxfoldersize 0|1000|2000|5000|10000]
|
||||
gam <UserTypeEntity> show imap|imap4
|
||||
|
||||
gam <UserTypeEntity> pop|pop3 <Boolean> [for allmail|newmail|mailfromnowon|fromnowown] [action keep|leaveininbox|archive|delete|trash|markread]
|
||||
gam <UserTypeEntity> show pop|pop3
|
||||
|
||||
gam <UserTypeEntity> language <Language>
|
||||
|
||||
gam <UserTypeEntity> pagesize 25|50|100
|
||||
|
||||
gam <UserTypeEntity> [add] sendas <EmailAddress> <Name> [replyto <EmailAddress>] [default] [treatasalias <Boolean>] [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <RegularExpression> <String>)*]
|
||||
gam <UserTypeEntity> update sendas <EmailAddress> [name <Name>] [replyto <EmailAddress>] [default] [treatasalias <Boolean>] [signature|sig <String>|(file <FileName> [charset <CharSet>]) (replace <RegularExpression> <String>)*]
|
||||
gam <UserTypeEntity> delete sendas <EmailAddress>
|
||||
gam <UserTypeEntity> show sendas [format]
|
||||
gam <UserTypeEntity> info sendas <EmailAddress> [format]
|
||||
gam <UserTypeEntity> print sendas [todrive]
|
||||
|
||||
gam <UserTypeEntity> shortcuts <Boolean>
|
||||
|
||||
gam <UserTypeEntity> signature|sig <String>|(file <FileName> [charset <Charset>]) (replace <Tag> <String>)* [name <String>] [replyto <EmailAddress>]
|
||||
gam <UserTypeEntity> show signature|sig [format]
|
||||
|
||||
gam <UserTypeEntity> snippets <Boolean>
|
||||
|
||||
gam <UserTypeEntity> utf|utf8|utf-8|unicode <Boolean>
|
||||
|
||||
gam <UserTypeEntity> vacation <FalseValues>
|
||||
gam <UserTypeEntity> vacation <TrueValues> subject <String> (message <String>)|(file <FileName> [charset <CharSet>]) (replace <Tag> <String>)* [html]
|
||||
[contactsonly] [domainonly] [startdate <Date>] [enddate <Date>]
|
||||
gam <UserTypeEntity> show vacation [format]
|
||||
|
||||
gam <UserTypeEntity> webclips <Boolean>
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "admin-settings:v1",
|
||||
"id": "admin-settings:v2",
|
||||
"name": "admin-settings",
|
||||
"version": "v1",
|
||||
"revision": "20130823",
|
||||
"title": "Admin Settings API (read-only calls)",
|
||||
"version": "v2",
|
||||
"revision": "20160616",
|
||||
"title": "Admin Settings API",
|
||||
"description": "Lets you access Google Apps Admin Settings",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
@@ -181,7 +181,8 @@ class AtomService(object):
|
||||
if content_length:
|
||||
all_headers['Content-Length'] = str(content_length)
|
||||
|
||||
all_headers['GData-Version'] = '2.0'
|
||||
if 'GData-Version' not in all_headers:
|
||||
all_headers['GData-Version'] = '2.0'
|
||||
# Find an Authorization token for this URL if one is available.
|
||||
if self.override_token:
|
||||
auth_token = self.override_token
|
||||
@@ -1,21 +1,17 @@
|
||||
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.py
|
||||
xcopy LICENSE gam\
|
||||
xcopy whatsnew.txt gam\
|
||||
xcopy cacert.pem gam\
|
||||
xcopy admin-settings-v1.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.py
|
||||
xcopy LICENSE gam-64\
|
||||
xcopy whatsnew.txt gam-64\
|
||||
xcopy cacert.pem gam-64\
|
||||
xcopy admin-settings-v1.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\
|
||||
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\
|
||||
"%ProgramFiles(x86)%\7-Zip\7z.exe" a -tzip gam-%1-windows-x64.zip gam-64\ -xr!.svn
|
||||
486
src/cloudprint-v2.json
Normal file
486
src/cloudprint-v2.json
Normal file
@@ -0,0 +1,486 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "cloudprint:v2",
|
||||
"name": "cloudprint",
|
||||
"version": "v2",
|
||||
"revision": "20150605",
|
||||
"title": "Cloud Print API",
|
||||
"description": "Lets you access Cloud Print Printers",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/cloud-print",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://www.google.com/",
|
||||
"basePath": "/cloudprint/",
|
||||
"rootUrl": "https://www.google.com/",
|
||||
"servicePath": "/cloudprint/",
|
||||
"parameters": {
|
||||
"prettyPrint": {
|
||||
"type": "boolean",
|
||||
"description": "Returns response with indentations and line breaks.",
|
||||
"default": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://www.googleapis.com/auth/cloudprint": {
|
||||
"description": "Manage Cloud Print"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"Job": {
|
||||
"id": "Job",
|
||||
"type": "object",
|
||||
"description": "Job Object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Job Title"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jobs": {
|
||||
"id": "Jobs",
|
||||
"type": "object",
|
||||
"description": "List of Jobs.",
|
||||
"properties": {
|
||||
"jobs": {
|
||||
"type": "array",
|
||||
"description": "List of job objects.",
|
||||
"items": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printer": {
|
||||
"id": "Printer",
|
||||
"type": "object",
|
||||
"description": "Printer Object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"description": "Display Name"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Printers": {
|
||||
"id": "Printers",
|
||||
"type": "object",
|
||||
"description": "List of Printers.",
|
||||
"properties": {
|
||||
"printers": {
|
||||
"type": "array",
|
||||
"description": "List of printer objects.",
|
||||
"items": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"jobs": {
|
||||
"methods": {
|
||||
"delete": {
|
||||
"id": "cloudprint.jobs.delete",
|
||||
"path": "deletejob",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"location": "query",
|
||||
"required": "true"
|
||||
}
|
||||
}
|
||||
},
|
||||
"fetch": {
|
||||
"id": "cloudprint.jobs.fetch",
|
||||
"path": "fetch",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"getticket": {
|
||||
"id": "cloudprint.jobs.getticket",
|
||||
"path": "ticket",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"use_cjt": {
|
||||
"type": "boolean",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.jobs.list",
|
||||
"path": "jobs",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"owner": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"q": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"offset": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"limit": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"sortorder": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.jobs.update",
|
||||
"path": "control",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"semantic_state_diff": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Jobs"
|
||||
}
|
||||
},
|
||||
"resubmit": {
|
||||
"id": "cloudprint.jobs.resubmit",
|
||||
"path": "resubmit",
|
||||
"httpMethod": "POST",
|
||||
"description": "resubmit a job to new printer.",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"jobid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"id": "cloudprint.jobs.submit",
|
||||
"path": "submit",
|
||||
"httpMethod": "POST",
|
||||
"description": "Send a print job to cloud print.",
|
||||
"request": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ticket": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"contentType": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"tag": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Job"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"printers": {
|
||||
"methods": {
|
||||
"get": {
|
||||
"id": "cloudprint.printers.get",
|
||||
"path": "printer",
|
||||
"httpMethod": "GET",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printer"
|
||||
}
|
||||
},
|
||||
"list": {
|
||||
"id": "cloudprint.printers.list",
|
||||
"path": "search",
|
||||
"httpMethod": "GET",
|
||||
"description": "List all printers",
|
||||
"parameters": {
|
||||
"q": {
|
||||
"type": "string",
|
||||
"description": "Query list of printers",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers of type",
|
||||
"location": "query"
|
||||
},
|
||||
"connection_status": {
|
||||
"type": "string",
|
||||
"description": "limit results to printers with this status",
|
||||
"location": "query"
|
||||
},
|
||||
"extra_fields": {
|
||||
"type": "string",
|
||||
"description": "include extra fields",
|
||||
"location": "query"
|
||||
}
|
||||
},
|
||||
"response": {
|
||||
"$ref": "Printers"
|
||||
}
|
||||
},
|
||||
"share": {
|
||||
"id": "cloudprint.printers.share",
|
||||
"path": "share",
|
||||
"httpMethod": "GET",
|
||||
"description": "Share printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"role": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"skip_notification": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unshare": {
|
||||
"id": "cloudprint.printers.unshare",
|
||||
"path": "unshare",
|
||||
"httpMethod": "GET",
|
||||
"description": "unshare printer with user, group or domain",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"id": "cloudprint.printers.delete",
|
||||
"path": "delete",
|
||||
"httpMethod": "GET",
|
||||
"description": "delete a printer",
|
||||
"parameters": {
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"id": "cloudprint.printers.update",
|
||||
"path": "update",
|
||||
"httpMethod": "GET",
|
||||
"description": "update a printer",
|
||||
"parameters": {
|
||||
"isTosAccepted": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"gcpVersion": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"setupUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"supportUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"firmware": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"currentQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"public": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"proxy": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"manufacturer": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"defaultDisplayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"uuid": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"updateUrl": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"model": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
},
|
||||
"printerid": {
|
||||
"type": "string",
|
||||
"required": "true",
|
||||
"location": "query"
|
||||
},
|
||||
"quotaEnabled": {
|
||||
"type": "boolean",
|
||||
"location": "query"
|
||||
},
|
||||
"dailyQuota": {
|
||||
"type": "string",
|
||||
"location": "query"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
34
src/email-audit-v1.json
Normal file
34
src/email-audit-v1.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "email-audit:v1",
|
||||
"name": "email-audit",
|
||||
"version": "v1",
|
||||
"revision": "20130823",
|
||||
"title": "Email Audit API",
|
||||
"description": "Lets you perform Google Apps email audits",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/email-audit",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/compliance/audit/",
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://apps-apis.google.com/a/feeds/compliance/audit/": {
|
||||
"description": "Manage email audits"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
},
|
||||
"resources": {
|
||||
}
|
||||
}
|
||||
34
src/email-settings-v2.json
Normal file
34
src/email-settings-v2.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"kind": "discovery#restDescription",
|
||||
"discoveryVersion": "v1",
|
||||
"id": "email-settings:v2",
|
||||
"name": "email-settings",
|
||||
"version": "v2",
|
||||
"revision": "20160616",
|
||||
"title": "Email Settings API",
|
||||
"description": "Lets you manage Google Apps Email Settings",
|
||||
"ownerDomain": "google.com",
|
||||
"ownerName": "Google",
|
||||
"icons": {
|
||||
"x16": "http://www.google.com/images/icons/product/search-16.gif",
|
||||
"x32": "http://www.google.com/images/icons/product/search-32.gif"
|
||||
},
|
||||
"documentationLink": "https://developers.google.com/admin-sdk/email-settings",
|
||||
"protocol": "rest",
|
||||
"baseUrl": "https://apps-apis.google.com/",
|
||||
"rootUrl": "https://apps-apis.google.com/",
|
||||
"servicePath": "/a/feeds/emailsettings/2.0/",
|
||||
"auth": {
|
||||
"oauth2": {
|
||||
"scopes": {
|
||||
"https://apps-apis.google.com/a/feeds/emailsettings/2.0/": {
|
||||
"description": "Manage email settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
},
|
||||
"resources": {
|
||||
}
|
||||
}
|
||||
11132
src/gam.py
Executable file
11132
src/gam.py
Executable file
File diff suppressed because it is too large
Load Diff
27
src/gam.spec
Normal file
27
src/gam.spec
Normal file
@@ -0,0 +1,27 @@
|
||||
# -*- mode: python -*-
|
||||
a = Analysis(['gam.py'],
|
||||
pathex=['C:\\Users\\jlee\\Documents\\GitHub\\GAM'],
|
||||
hiddenimports=[],
|
||||
hookspath=None,
|
||||
excludes=['_tkinter'],
|
||||
runtime_hooks=None)
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
a.datas += [('httplib2/cacerts.txt', 'httplib2\cacerts.txt', 'DATA')]
|
||||
a.datas += [('admin-settings-v2.json', 'admin-settings-v2.json', 'DATA')]
|
||||
a.datas += [('cloudprint-v2.json', 'cloudprint-v2.json', 'DATA')]
|
||||
a.datas += [('email-audit-v1.json', 'email-audit-v1.json', 'DATA')]
|
||||
a.datas += [('email-settings-v2.json', 'email-settings-v2.json', 'DATA')]
|
||||
pyz = PYZ(a.pure)
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
name='gam.exe',
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=True,
|
||||
console=True )
|
||||
@@ -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 <OAuthRequest>.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 <OAuthRequest>.to_header method since it returned non-oauth params
|
||||
as well which was incorrect. (Issue 31)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user