├── .gitignore
├── static
├── robots.txt
├── favicon.ico
└── apple-touch-icon.png
├── oauth2client
├── __init__.py
├── anyjson.py
├── gce.py
├── keyring_storage.py
├── file.py
├── xsrfutil.py
├── django_orm.py
├── clientsecrets.py
├── old_run.py
├── util.py
├── tools.py
├── crypt.py
├── locked_file.py
├── multistore_file.py
└── appengine.py
├── cron.yaml
├── index.yaml
├── app.yaml
├── database_tables.py
├── README.md
├── apiclient
├── __init__.py
├── sample_tools.py
├── errors.py
├── mimeparse.py
├── schema.py
├── channel.py
└── model.py
├── Acknowledgements.txt
├── license.txt
├── youtube_integration.py
├── httplib2
├── iri2uri.py
└── socks.py
├── uritemplate
└── __init__.py
└── main.py
/.gitignore:
--------------------------------------------------------------------------------
1 | api_key.txt
2 | **.pyc
3 |
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow: /update
3 | Disallow: /update_push
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncurrault/brady-vs-grey/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/static/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ncurrault/brady-vs-grey/HEAD/static/apple-touch-icon.png
--------------------------------------------------------------------------------
/oauth2client/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "1.2"
2 |
3 | GOOGLE_AUTH_URI = 'https://accounts.google.com/o/oauth2/auth'
4 | GOOGLE_REVOKE_URI = 'https://accounts.google.com/o/oauth2/revoke'
5 | GOOGLE_TOKEN_URI = 'https://accounts.google.com/o/oauth2/token'
6 |
--------------------------------------------------------------------------------
/cron.yaml:
--------------------------------------------------------------------------------
1 | cron:
2 | - description: 4x daily update to push to the database
3 | url: /update
4 | schedule: every 6 hours from 00:00 to 23:59
5 | - description: hourly database query to update memcache; always at least 5 min after an update
6 | url: /update_push
7 | schedule: every 1 hours from 00:05 to 23:10
--------------------------------------------------------------------------------
/index.yaml:
--------------------------------------------------------------------------------
1 | indexes:
2 |
3 | # AUTOGENERATED
4 |
5 | # This index.yaml is automatically updated whenever the dev_appserver
6 | # detects that a new type of query is run. If you want to manage the
7 | # index.yaml file manually, remove the above marker line (the line
8 | # saying "# AUTOGENERATED"). If you want to manage some indexes
9 | # manually, move them above the marker line. The index.yaml file is
10 | # automatically uploaded to the admin console when you next deploy
11 | # your application using appcfg.py.
12 |
--------------------------------------------------------------------------------
/app.yaml:
--------------------------------------------------------------------------------
1 | application: brady-vs-grey
2 | version: 1
3 | runtime: python27
4 | api_version: 1
5 | threadsafe: yes
6 |
7 | handlers:
8 | - url: /favicon\.ico
9 | static_files: static/favicon.ico
10 | upload: static/favicon.ico
11 |
12 | - url: /apple-touch-icon\.png
13 | static_files: static/apple-touch-icon.png
14 | upload: static/apple-touch-icon.png
15 |
16 | - url: /robots\.txt
17 | static_files: static/robots.txt
18 | upload: static/robots.txt
19 |
20 | - url: .*
21 | script: main.app
22 |
23 | libraries:
24 | - name: webapp2
25 | version: "2.5.2"
26 |
--------------------------------------------------------------------------------
/database_tables.py:
--------------------------------------------------------------------------------
1 | from google.appengine.ext import db
2 |
3 | class Video(db.Model):
4 | yt_id = db.StringProperty()
5 | title = db.StringProperty()
6 | published = db.DateTimeProperty()
7 | channel = db.StringProperty()
8 |
9 | viewcount = db.IntegerProperty()
10 | # [0,inf) for actual view count
11 | # -1 for live videos
12 | # -2 for not yet calculated
13 | # -3 for misc. errors
14 |
15 |
16 | class BradyVideo(Video):
17 | pass
18 | class GreyVideo(Video):
19 | pass
20 |
21 | class UpdateLog(db.Model):
22 | update_time = db.DateTimeProperty(auto_now=True)
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Brady vs. Grey
2 |
3 | ## API Key Hiding (`api_key.txt`)
4 |
5 | To make sure that my own YouTube Data API key was not published,
6 | I separated it into a text file (`api_key.txt`) and used `.gitignore` to ensure that it wasn't published.
7 |
8 | If you wish to do this yourself, you must
9 |
10 | 1. [Get your own API key from Google](http://developers.google.com/youtube/v3/getting-started#intro).
11 | 2. Put this key in a file entitled `api_key.txt`.
12 | 3. Put this file in the same directory as `youtube_integration.py`.
13 |
14 |
15 | ## Enjoy!
16 |
17 | You can view the running version [here](http://brady-vs-grey.appspot.com).
18 |
--------------------------------------------------------------------------------
/apiclient/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | __version__ = "1.2"
16 |
--------------------------------------------------------------------------------
/Acknowledgements.txt:
--------------------------------------------------------------------------------
1 | The following software may be included in this product:
2 | Google APIs Client Library for Python; Use of any of this software is governed by the terms of the license below:
3 |
4 | Copyright 2014 Google Inc. All Rights Reserved.
5 |
6 | Licensed under the Apache License, Version 2.0 (the "License");
7 | you may not use this file except in compliance with the License.
8 | You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing, software
13 | distributed under the License is distributed on an "AS IS" BASIS,
14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | See the License for the specific language governing permissions and
16 | limitations under the License.
17 |
18 | Dependent Modules
19 | =================
20 |
21 | This code has the following dependencies
22 | above and beyond the Python standard library:
23 |
24 | uritemplates - Apache License 2.0
25 | httplib2 - MIT License
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Nicholas Currault
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/oauth2client/anyjson.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utility module to import a JSON module
16 |
17 | Hides all the messy details of exactly where
18 | we get a simplejson module from.
19 | """
20 |
21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
22 |
23 |
24 | try: # pragma: no cover
25 | # Should work for Python2.6 and higher.
26 | import json as simplejson
27 | except ImportError: # pragma: no cover
28 | try:
29 | import simplejson
30 | except ImportError:
31 | # Try to import from django, should work on App Engine
32 | from django.utils import simplejson
33 |
--------------------------------------------------------------------------------
/oauth2client/gce.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for Google Compute Engine
16 |
17 | Utilities for making it easier to use OAuth 2.0 on Google Compute Engine.
18 | """
19 |
20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 |
22 | import httplib2
23 | import logging
24 | import uritemplate
25 |
26 | from oauth2client import util
27 | from oauth2client.anyjson import simplejson
28 | from oauth2client.client import AccessTokenRefreshError
29 | from oauth2client.client import AssertionCredentials
30 |
31 | logger = logging.getLogger(__name__)
32 |
33 | # URI Template for the endpoint that returns access_tokens.
34 | META = ('http://metadata.google.internal/0.1/meta-data/service-accounts/'
35 | 'default/acquire{?scope}')
36 |
37 |
38 | class AppAssertionCredentials(AssertionCredentials):
39 | """Credentials object for Compute Engine Assertion Grants
40 |
41 | This object will allow a Compute Engine instance to identify itself to
42 | Google and other OAuth 2.0 servers that can verify assertions. It can be used
43 | for the purpose of accessing data stored under an account assigned to the
44 | Compute Engine instance itself.
45 |
46 | This credential does not require a flow to instantiate because it represents
47 | a two legged flow, and therefore has all of the required information to
48 | generate and refresh its own access tokens.
49 | """
50 |
51 | @util.positional(2)
52 | def __init__(self, scope, **kwargs):
53 | """Constructor for AppAssertionCredentials
54 |
55 | Args:
56 | scope: string or iterable of strings, scope(s) of the credentials being
57 | requested.
58 | """
59 | self.scope = util.scopes_to_string(scope)
60 |
61 | # Assertion type is no longer used, but still in the parent class signature.
62 | super(AppAssertionCredentials, self).__init__(None)
63 |
64 | @classmethod
65 | def from_json(cls, json):
66 | data = simplejson.loads(json)
67 | return AppAssertionCredentials(data['scope'])
68 |
69 | def _refresh(self, http_request):
70 | """Refreshes the access_token.
71 |
72 | Skip all the storage hoops and just refresh using the API.
73 |
74 | Args:
75 | http_request: callable, a callable that matches the method signature of
76 | httplib2.Http.request, used to make the refresh request.
77 |
78 | Raises:
79 | AccessTokenRefreshError: When the refresh fails.
80 | """
81 | uri = uritemplate.expand(META, {'scope': self.scope})
82 | response, content = http_request(uri)
83 | if response.status == 200:
84 | try:
85 | d = simplejson.loads(content)
86 | except StandardError, e:
87 | raise AccessTokenRefreshError(str(e))
88 | self.access_token = d['accessToken']
89 | else:
90 | raise AccessTokenRefreshError(content)
91 |
--------------------------------------------------------------------------------
/oauth2client/keyring_storage.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2012 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """A keyring based Storage.
16 |
17 | A Storage for Credentials that uses the keyring module.
18 | """
19 |
20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 |
22 | import keyring
23 | import threading
24 |
25 | from client import Storage as BaseStorage
26 | from client import Credentials
27 |
28 |
29 | class Storage(BaseStorage):
30 | """Store and retrieve a single credential to and from the keyring.
31 |
32 | To use this module you must have the keyring module installed. See
33 | . This is an optional module and is not
34 | installed with oauth2client by default because it does not work on all the
35 | platforms that oauth2client supports, such as Google App Engine.
36 |
37 | The keyring module is a cross-platform
38 | library for access the keyring capabilities of the local system. The user will
39 | be prompted for their keyring password when this module is used, and the
40 | manner in which the user is prompted will vary per platform.
41 |
42 | Usage:
43 | from oauth2client.keyring_storage import Storage
44 |
45 | s = Storage('name_of_application', 'user1')
46 | credentials = s.get()
47 |
48 | """
49 |
50 | def __init__(self, service_name, user_name):
51 | """Constructor.
52 |
53 | Args:
54 | service_name: string, The name of the service under which the credentials
55 | are stored.
56 | user_name: string, The name of the user to store credentials for.
57 | """
58 | self._service_name = service_name
59 | self._user_name = user_name
60 | self._lock = threading.Lock()
61 |
62 | def acquire_lock(self):
63 | """Acquires any lock necessary to access this Storage.
64 |
65 | This lock is not reentrant."""
66 | self._lock.acquire()
67 |
68 | def release_lock(self):
69 | """Release the Storage lock.
70 |
71 | Trying to release a lock that isn't held will result in a
72 | RuntimeError.
73 | """
74 | self._lock.release()
75 |
76 | def locked_get(self):
77 | """Retrieve Credential from file.
78 |
79 | Returns:
80 | oauth2client.client.Credentials
81 | """
82 | credentials = None
83 | content = keyring.get_password(self._service_name, self._user_name)
84 |
85 | if content is not None:
86 | try:
87 | credentials = Credentials.new_from_json(content)
88 | credentials.set_store(self)
89 | except ValueError:
90 | pass
91 |
92 | return credentials
93 |
94 | def locked_put(self, credentials):
95 | """Write Credentials to file.
96 |
97 | Args:
98 | credentials: Credentials, the credentials to store.
99 | """
100 | keyring.set_password(self._service_name, self._user_name,
101 | credentials.to_json())
102 |
103 | def locked_delete(self):
104 | """Delete Credentials file.
105 |
106 | Args:
107 | credentials: Credentials, the credentials to store.
108 | """
109 | keyring.set_password(self._service_name, self._user_name, '')
110 |
--------------------------------------------------------------------------------
/oauth2client/file.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for OAuth.
16 |
17 | Utilities for making it easier to work with OAuth 2.0
18 | credentials.
19 | """
20 |
21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
22 |
23 | import os
24 | import stat
25 | import threading
26 |
27 | from anyjson import simplejson
28 | from client import Storage as BaseStorage
29 | from client import Credentials
30 |
31 |
32 | class CredentialsFileSymbolicLinkError(Exception):
33 | """Credentials files must not be symbolic links."""
34 |
35 |
36 | class Storage(BaseStorage):
37 | """Store and retrieve a single credential to and from a file."""
38 |
39 | def __init__(self, filename):
40 | self._filename = filename
41 | self._lock = threading.Lock()
42 |
43 | def _validate_file(self):
44 | if os.path.islink(self._filename):
45 | raise CredentialsFileSymbolicLinkError(
46 | 'File: %s is a symbolic link.' % self._filename)
47 |
48 | def acquire_lock(self):
49 | """Acquires any lock necessary to access this Storage.
50 |
51 | This lock is not reentrant."""
52 | self._lock.acquire()
53 |
54 | def release_lock(self):
55 | """Release the Storage lock.
56 |
57 | Trying to release a lock that isn't held will result in a
58 | RuntimeError.
59 | """
60 | self._lock.release()
61 |
62 | def locked_get(self):
63 | """Retrieve Credential from file.
64 |
65 | Returns:
66 | oauth2client.client.Credentials
67 |
68 | Raises:
69 | CredentialsFileSymbolicLinkError if the file is a symbolic link.
70 | """
71 | credentials = None
72 | self._validate_file()
73 | try:
74 | f = open(self._filename, 'rb')
75 | content = f.read()
76 | f.close()
77 | except IOError:
78 | return credentials
79 |
80 | try:
81 | credentials = Credentials.new_from_json(content)
82 | credentials.set_store(self)
83 | except ValueError:
84 | pass
85 |
86 | return credentials
87 |
88 | def _create_file_if_needed(self):
89 | """Create an empty file if necessary.
90 |
91 | This method will not initialize the file. Instead it implements a
92 | simple version of "touch" to ensure the file has been created.
93 | """
94 | if not os.path.exists(self._filename):
95 | old_umask = os.umask(0177)
96 | try:
97 | open(self._filename, 'a+b').close()
98 | finally:
99 | os.umask(old_umask)
100 |
101 | def locked_put(self, credentials):
102 | """Write Credentials to file.
103 |
104 | Args:
105 | credentials: Credentials, the credentials to store.
106 |
107 | Raises:
108 | CredentialsFileSymbolicLinkError if the file is a symbolic link.
109 | """
110 |
111 | self._create_file_if_needed()
112 | self._validate_file()
113 | f = open(self._filename, 'wb')
114 | f.write(credentials.to_json())
115 | f.close()
116 |
117 | def locked_delete(self):
118 | """Delete Credentials file.
119 |
120 | Args:
121 | credentials: Credentials, the credentials to store.
122 | """
123 |
124 | os.unlink(self._filename)
125 |
--------------------------------------------------------------------------------
/apiclient/sample_tools.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2013 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for making samples.
16 |
17 | Consolidates a lot of code commonly repeated in sample applications.
18 | """
19 |
20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 | __all__ = ['init']
22 |
23 |
24 | import argparse
25 | import httplib2
26 | import os
27 |
28 | from apiclient import discovery
29 | from oauth2client import client
30 | from oauth2client import file
31 | from oauth2client import tools
32 |
33 |
34 | def init(argv, name, version, doc, filename, scope=None, parents=[]):
35 | """A common initialization routine for samples.
36 |
37 | Many of the sample applications do the same initialization, which has now
38 | been consolidated into this function. This function uses common idioms found
39 | in almost all the samples, i.e. for an API with name 'apiname', the
40 | credentials are stored in a file named apiname.dat, and the
41 | client_secrets.json file is stored in the same directory as the application
42 | main file.
43 |
44 | Args:
45 | argv: list of string, the command-line parameters of the application.
46 | name: string, name of the API.
47 | version: string, version of the API.
48 | doc: string, description of the application. Usually set to __doc__.
49 | file: string, filename of the application. Usually set to __file__.
50 | parents: list of argparse.ArgumentParser, additional command-line flags.
51 | scope: string, The OAuth scope used.
52 |
53 | Returns:
54 | A tuple of (service, flags), where service is the service object and flags
55 | is the parsed command-line flags.
56 | """
57 | if scope is None:
58 | scope = 'https://www.googleapis.com/auth/' + name
59 |
60 | # Parser command-line arguments.
61 | parent_parsers = [tools.argparser]
62 | parent_parsers.extend(parents)
63 | parser = argparse.ArgumentParser(
64 | description=doc,
65 | formatter_class=argparse.RawDescriptionHelpFormatter,
66 | parents=parent_parsers)
67 | flags = parser.parse_args(argv[1:])
68 |
69 | # Name of a file containing the OAuth 2.0 information for this
70 | # application, including client_id and client_secret, which are found
71 | # on the API Access tab on the Google APIs
72 | # Console .
73 | client_secrets = os.path.join(os.path.dirname(filename),
74 | 'client_secrets.json')
75 |
76 | # Set up a Flow object to be used if we need to authenticate.
77 | flow = client.flow_from_clientsecrets(client_secrets,
78 | scope=scope,
79 | message=tools.message_if_missing(client_secrets))
80 |
81 | # Prepare credentials, and authorize HTTP object with them.
82 | # If the credentials don't exist or are invalid run through the native client
83 | # flow. The Storage object will ensure that if successful the good
84 | # credentials will get written back to a file.
85 | storage = file.Storage(name + '.dat')
86 | credentials = storage.get()
87 | if credentials is None or credentials.invalid:
88 | credentials = tools.run_flow(flow, storage, flags)
89 | http = credentials.authorize(http = httplib2.Http())
90 |
91 | # Construct a service object via the discovery service.
92 | service = discovery.build(name, version, http=http)
93 | return (service, flags)
94 |
--------------------------------------------------------------------------------
/youtube_integration.py:
--------------------------------------------------------------------------------
1 | import os
2 | import urllib
3 |
4 | from apiclient.discovery import build
5 | from optparse import OptionParser
6 |
7 | import json
8 |
9 | import datetime
10 | import time
11 |
12 | from database_tables import *
13 |
14 | REGISTRATION_INSTRUCTIONS = """
15 | You must set up a project and get an API key to run this code. Please see
16 | the instructions for creating a project and a key at https://developers.google.com/youtube/registering_an_application.
19 |
20 | Make sure that you have enabled the YouTube Data API (v3) and the Freebase
21 | API for your project."""
22 |
23 | # Set API_KEY to the "API key" value from the "Access" tab of the
24 | # Google APIs Console http://code.google.com/apis/console#access
25 | # Please ensure that you have enabled the YouTube Data API and Freebase API
26 | # for your project.
27 | with open('api_key.txt', 'r') as key_file:
28 | API_KEY = key_file.read()
29 |
30 | YOUTUBE_API_SERVICE_NAME = "youtube"
31 | YOUTUBE_API_VERSION = "v3"
32 | FREEBASE_SEARCH_URL = "https://www.googleapis.com/freebase/v1/search?%s"
33 | QUERY_TERM = "dog"
34 |
35 |
36 |
37 | def get_view_count(vid_id):
38 | try:
39 | url = "https://www.googleapis.com/youtube/v3/videos?id=%(id)s&key=%(key)s&fields=items(id,snippet(channelId,title,categoryId),statistics)&part=snippet,statistics" % {'key':API_KEY, 'id':vid_id}
40 | p = urllib.urlopen(url)
41 | json_data = p.read()
42 | p.close()
43 | json_data = json.loads(json_data)
44 |
45 | views = int(json_data['items'][0]['statistics']['viewCount'])
46 | return views
47 | except KeyError:
48 | return -1 # For live videos
49 | except:
50 | return -3 # For misc. errors
51 |
52 |
53 | def get_vids(input_channel_name, save_class='Video'):
54 | # Service for calling the YouTube API
55 | youtube = build(YOUTUBE_API_SERVICE_NAME,
56 | YOUTUBE_API_VERSION,
57 | developerKey=API_KEY)
58 |
59 | # Use form inputs to create request params for channel details
60 | channels_response = None
61 |
62 | channels_response = youtube.channels().list(
63 | forUsername=input_channel_name,
64 | part='snippet,contentDetails'
65 | ).execute()
66 |
67 | channel_name = ''
68 | videos = []
69 |
70 | for channel in channels_response['items']:
71 | uploads_list_id = channel['contentDetails']['relatedPlaylists']['uploads']
72 | channel_name = channel['snippet']['title']
73 |
74 | next_page_token = ''
75 | while next_page_token is not None:
76 | playlistitems_response = youtube.playlistItems().list(
77 | playlistId=uploads_list_id,
78 | part='snippet',
79 | maxResults=50,
80 | pageToken=next_page_token
81 | ).execute()
82 |
83 | for playlist_item in playlistitems_response['items']:
84 | videos.append(playlist_item)
85 |
86 | next_page_token = playlistitems_response.get('tokenPagination', {}).get(
87 | 'nextPageToken')
88 |
89 | if save_class == 'BradyVideo' and len(videos) > 20: # One Brady's channels shouldn't have MORE THAN 20 that are necessary
90 | break
91 | elif save_class == 'GreyVideo' and len(videos) > 1: # For Grey's channels, you only need the most recent video
92 | break
93 | elif len(videos) > 100: # This shouldn't actually happen
94 | break
95 |
96 |
97 |
98 | return [ \
99 | eval(save_class)(
100 | title=e[u'snippet'][u'title'], \
101 | channel=input_channel_name, \
102 | published=(datetime.datetime.strptime(e[u'snippet'][u'publishedAt'][:-5],'%Y-%m-%dT%H:%M:%S')), \
103 | yt_id=(e[u'snippet'][u'resourceId'][u'videoId']), \
104 | viewcount=-2 \
105 | ) \
106 | for e in videos ]
--------------------------------------------------------------------------------
/oauth2client/xsrfutil.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2.5
2 | #
3 | # Copyright 2010 the Melange authors.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """Helper methods for creating & verifying XSRF tokens."""
18 |
19 | __authors__ = [
20 | '"Doug Coker" ',
21 | '"Joe Gregorio" ',
22 | ]
23 |
24 |
25 | import base64
26 | import hmac
27 | import os # for urandom
28 | import time
29 |
30 | from oauth2client import util
31 |
32 |
33 | # Delimiter character
34 | DELIMITER = ':'
35 |
36 | # 1 hour in seconds
37 | DEFAULT_TIMEOUT_SECS = 1*60*60
38 |
39 | @util.positional(2)
40 | def generate_token(key, user_id, action_id="", when=None):
41 | """Generates a URL-safe token for the given user, action, time tuple.
42 |
43 | Args:
44 | key: secret key to use.
45 | user_id: the user ID of the authenticated user.
46 | action_id: a string identifier of the action they requested
47 | authorization for.
48 | when: the time in seconds since the epoch at which the user was
49 | authorized for this action. If not set the current time is used.
50 |
51 | Returns:
52 | A string XSRF protection token.
53 | """
54 | when = when or int(time.time())
55 | digester = hmac.new(key)
56 | digester.update(str(user_id))
57 | digester.update(DELIMITER)
58 | digester.update(action_id)
59 | digester.update(DELIMITER)
60 | digester.update(str(when))
61 | digest = digester.digest()
62 |
63 | token = base64.urlsafe_b64encode('%s%s%d' % (digest,
64 | DELIMITER,
65 | when))
66 | return token
67 |
68 |
69 | @util.positional(3)
70 | def validate_token(key, token, user_id, action_id="", current_time=None):
71 | """Validates that the given token authorizes the user for the action.
72 |
73 | Tokens are invalid if the time of issue is too old or if the token
74 | does not match what generateToken outputs (i.e. the token was forged).
75 |
76 | Args:
77 | key: secret key to use.
78 | token: a string of the token generated by generateToken.
79 | user_id: the user ID of the authenticated user.
80 | action_id: a string identifier of the action they requested
81 | authorization for.
82 |
83 | Returns:
84 | A boolean - True if the user is authorized for the action, False
85 | otherwise.
86 | """
87 | if not token:
88 | return False
89 | try:
90 | decoded = base64.urlsafe_b64decode(str(token))
91 | token_time = long(decoded.split(DELIMITER)[-1])
92 | except (TypeError, ValueError):
93 | return False
94 | if current_time is None:
95 | current_time = time.time()
96 | # If the token is too old it's not valid.
97 | if current_time - token_time > DEFAULT_TIMEOUT_SECS:
98 | return False
99 |
100 | # The given token should match the generated one with the same time.
101 | expected_token = generate_token(key, user_id, action_id=action_id,
102 | when=token_time)
103 | if len(token) != len(expected_token):
104 | return False
105 |
106 | # Perform constant time comparison to avoid timing attacks
107 | different = 0
108 | for x, y in zip(token, expected_token):
109 | different |= ord(x) ^ ord(y)
110 | if different:
111 | return False
112 |
113 | return True
114 |
--------------------------------------------------------------------------------
/apiclient/errors.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2.4
2 | #
3 | # Copyright (C) 2010 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """Errors for the library.
18 |
19 | All exceptions defined by the library
20 | should be defined in this file.
21 | """
22 |
23 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
24 |
25 |
26 | from oauth2client import util
27 | from oauth2client.anyjson import simplejson
28 |
29 |
30 | class Error(Exception):
31 | """Base error for this module."""
32 | pass
33 |
34 |
35 | class HttpError(Error):
36 | """HTTP data was invalid or unexpected."""
37 |
38 | @util.positional(3)
39 | def __init__(self, resp, content, uri=None):
40 | self.resp = resp
41 | self.content = content
42 | self.uri = uri
43 |
44 | def _get_reason(self):
45 | """Calculate the reason for the error from the response content."""
46 | reason = self.resp.reason
47 | try:
48 | data = simplejson.loads(self.content)
49 | reason = data['error']['message']
50 | except (ValueError, KeyError):
51 | pass
52 | if reason is None:
53 | reason = ''
54 | return reason
55 |
56 | def __repr__(self):
57 | if self.uri:
58 | return '' % (
59 | self.resp.status, self.uri, self._get_reason().strip())
60 | else:
61 | return '' % (self.resp.status, self._get_reason())
62 |
63 | __str__ = __repr__
64 |
65 |
66 | class InvalidJsonError(Error):
67 | """The JSON returned could not be parsed."""
68 | pass
69 |
70 |
71 | class UnknownFileType(Error):
72 | """File type unknown or unexpected."""
73 | pass
74 |
75 |
76 | class UnknownLinkType(Error):
77 | """Link type unknown or unexpected."""
78 | pass
79 |
80 |
81 | class UnknownApiNameOrVersion(Error):
82 | """No API with that name and version exists."""
83 | pass
84 |
85 |
86 | class UnacceptableMimeTypeError(Error):
87 | """That is an unacceptable mimetype for this operation."""
88 | pass
89 |
90 |
91 | class MediaUploadSizeError(Error):
92 | """Media is larger than the method can accept."""
93 | pass
94 |
95 |
96 | class ResumableUploadError(HttpError):
97 | """Error occured during resumable upload."""
98 | pass
99 |
100 |
101 | class InvalidChunkSizeError(Error):
102 | """The given chunksize is not valid."""
103 | pass
104 |
105 | class InvalidNotificationError(Error):
106 | """The channel Notification is invalid."""
107 | pass
108 |
109 | class BatchError(HttpError):
110 | """Error occured during batch operations."""
111 |
112 | @util.positional(2)
113 | def __init__(self, reason, resp=None, content=None):
114 | self.resp = resp
115 | self.content = content
116 | self.reason = reason
117 |
118 | def __repr__(self):
119 | return '' % (self.resp.status, self.reason)
120 |
121 | __str__ = __repr__
122 |
123 |
124 | class UnexpectedMethodError(Error):
125 | """Exception raised by RequestMockBuilder on unexpected calls."""
126 |
127 | @util.positional(1)
128 | def __init__(self, methodId=None):
129 | """Constructor for an UnexpectedMethodError."""
130 | super(UnexpectedMethodError, self).__init__(
131 | 'Received unexpected call %s' % methodId)
132 |
133 |
134 | class UnexpectedBodyError(Error):
135 | """Exception raised by RequestMockBuilder on unexpected bodies."""
136 |
137 | def __init__(self, expected, provided):
138 | """Constructor for an UnexpectedMethodError."""
139 | super(UnexpectedBodyError, self).__init__(
140 | 'Expected: [%s] - Provided: [%s]' % (expected, provided))
141 |
--------------------------------------------------------------------------------
/httplib2/iri2uri.py:
--------------------------------------------------------------------------------
1 | """
2 | iri2uri
3 |
4 | Converts an IRI to a URI.
5 |
6 | """
7 | __author__ = "Joe Gregorio (joe@bitworking.org)"
8 | __copyright__ = "Copyright 2006, Joe Gregorio"
9 | __contributors__ = []
10 | __version__ = "1.0.0"
11 | __license__ = "MIT"
12 | __history__ = """
13 | """
14 |
15 | import urlparse
16 |
17 |
18 | # Convert an IRI to a URI following the rules in RFC 3987
19 | #
20 | # The characters we need to enocde and escape are defined in the spec:
21 | #
22 | # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD
23 | # ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF
24 | # / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD
25 | # / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD
26 | # / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD
27 | # / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD
28 | # / %xD0000-DFFFD / %xE1000-EFFFD
29 |
30 | escape_range = [
31 | (0xA0, 0xD7FF),
32 | (0xE000, 0xF8FF),
33 | (0xF900, 0xFDCF),
34 | (0xFDF0, 0xFFEF),
35 | (0x10000, 0x1FFFD),
36 | (0x20000, 0x2FFFD),
37 | (0x30000, 0x3FFFD),
38 | (0x40000, 0x4FFFD),
39 | (0x50000, 0x5FFFD),
40 | (0x60000, 0x6FFFD),
41 | (0x70000, 0x7FFFD),
42 | (0x80000, 0x8FFFD),
43 | (0x90000, 0x9FFFD),
44 | (0xA0000, 0xAFFFD),
45 | (0xB0000, 0xBFFFD),
46 | (0xC0000, 0xCFFFD),
47 | (0xD0000, 0xDFFFD),
48 | (0xE1000, 0xEFFFD),
49 | (0xF0000, 0xFFFFD),
50 | (0x100000, 0x10FFFD),
51 | ]
52 |
53 | def encode(c):
54 | retval = c
55 | i = ord(c)
56 | for low, high in escape_range:
57 | if i < low:
58 | break
59 | if i >= low and i <= high:
60 | retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')])
61 | break
62 | return retval
63 |
64 |
65 | def iri2uri(uri):
66 | """Convert an IRI to a URI. Note that IRIs must be
67 | passed in a unicode strings. That is, do not utf-8 encode
68 | the IRI before passing it into the function."""
69 | if isinstance(uri ,unicode):
70 | (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri)
71 | authority = authority.encode('idna')
72 | # For each character in 'ucschar' or 'iprivate'
73 | # 1. encode as utf-8
74 | # 2. then %-encode each octet of that utf-8
75 | uri = urlparse.urlunsplit((scheme, authority, path, query, fragment))
76 | uri = "".join([encode(c) for c in uri])
77 | return uri
78 |
79 | if __name__ == "__main__":
80 | import unittest
81 |
82 | class Test(unittest.TestCase):
83 |
84 | def test_uris(self):
85 | """Test that URIs are invariant under the transformation."""
86 | invariant = [
87 | u"ftp://ftp.is.co.za/rfc/rfc1808.txt",
88 | u"http://www.ietf.org/rfc/rfc2396.txt",
89 | u"ldap://[2001:db8::7]/c=GB?objectClass?one",
90 | u"mailto:John.Doe@example.com",
91 | u"news:comp.infosystems.www.servers.unix",
92 | u"tel:+1-816-555-1212",
93 | u"telnet://192.0.2.16:80/",
94 | u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ]
95 | for uri in invariant:
96 | self.assertEqual(uri, iri2uri(uri))
97 |
98 | def test_iri(self):
99 | """ Test that the right type of escaping is done for each part of the URI."""
100 | self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}"))
101 | self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}"))
102 | self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}"))
103 | self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}"))
104 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))
105 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")))
106 | self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8')))
107 |
108 | unittest.main()
109 |
110 |
111 |
--------------------------------------------------------------------------------
/oauth2client/django_orm.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """OAuth 2.0 utilities for Django.
16 |
17 | Utilities for using OAuth 2.0 in conjunction with
18 | the Django datastore.
19 | """
20 |
21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
22 |
23 | import oauth2client
24 | import base64
25 | import pickle
26 |
27 | from django.db import models
28 | from oauth2client.client import Storage as BaseStorage
29 |
30 | class CredentialsField(models.Field):
31 |
32 | __metaclass__ = models.SubfieldBase
33 |
34 | def __init__(self, *args, **kwargs):
35 | if 'null' not in kwargs:
36 | kwargs['null'] = True
37 | super(CredentialsField, self).__init__(*args, **kwargs)
38 |
39 | def get_internal_type(self):
40 | return "TextField"
41 |
42 | def to_python(self, value):
43 | if value is None:
44 | return None
45 | if isinstance(value, oauth2client.client.Credentials):
46 | return value
47 | return pickle.loads(base64.b64decode(value))
48 |
49 | def get_db_prep_value(self, value, connection, prepared=False):
50 | if value is None:
51 | return None
52 | return base64.b64encode(pickle.dumps(value))
53 |
54 |
55 | class FlowField(models.Field):
56 |
57 | __metaclass__ = models.SubfieldBase
58 |
59 | def __init__(self, *args, **kwargs):
60 | if 'null' not in kwargs:
61 | kwargs['null'] = True
62 | super(FlowField, self).__init__(*args, **kwargs)
63 |
64 | def get_internal_type(self):
65 | return "TextField"
66 |
67 | def to_python(self, value):
68 | if value is None:
69 | return None
70 | if isinstance(value, oauth2client.client.Flow):
71 | return value
72 | return pickle.loads(base64.b64decode(value))
73 |
74 | def get_db_prep_value(self, value, connection, prepared=False):
75 | if value is None:
76 | return None
77 | return base64.b64encode(pickle.dumps(value))
78 |
79 |
80 | class Storage(BaseStorage):
81 | """Store and retrieve a single credential to and from
82 | the datastore.
83 |
84 | This Storage helper presumes the Credentials
85 | have been stored as a CredenialsField
86 | on a db model class.
87 | """
88 |
89 | def __init__(self, model_class, key_name, key_value, property_name):
90 | """Constructor for Storage.
91 |
92 | Args:
93 | model: db.Model, model class
94 | key_name: string, key name for the entity that has the credentials
95 | key_value: string, key value for the entity that has the credentials
96 | property_name: string, name of the property that is an CredentialsProperty
97 | """
98 | self.model_class = model_class
99 | self.key_name = key_name
100 | self.key_value = key_value
101 | self.property_name = property_name
102 |
103 | def locked_get(self):
104 | """Retrieve Credential from datastore.
105 |
106 | Returns:
107 | oauth2client.Credentials
108 | """
109 | credential = None
110 |
111 | query = {self.key_name: self.key_value}
112 | entities = self.model_class.objects.filter(**query)
113 | if len(entities) > 0:
114 | credential = getattr(entities[0], self.property_name)
115 | if credential and hasattr(credential, 'set_store'):
116 | credential.set_store(self)
117 | return credential
118 |
119 | def locked_put(self, credentials):
120 | """Write a Credentials to the datastore.
121 |
122 | Args:
123 | credentials: Credentials, the credentials to store.
124 | """
125 | args = {self.key_name: self.key_value}
126 | entity = self.model_class(**args)
127 | setattr(entity, self.property_name, credentials)
128 | entity.save()
129 |
130 | def locked_delete(self):
131 | """Delete Credentials from the datastore."""
132 |
133 | query = {self.key_name: self.key_value}
134 | entities = self.model_class.objects.filter(**query).delete()
135 |
--------------------------------------------------------------------------------
/oauth2client/clientsecrets.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2011 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for reading OAuth 2.0 client secret files.
16 |
17 | A client_secrets.json file contains all the information needed to interact with
18 | an OAuth 2.0 protected service.
19 | """
20 |
21 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
22 |
23 |
24 | from anyjson import simplejson
25 |
26 | # Properties that make a client_secrets.json file valid.
27 | TYPE_WEB = 'web'
28 | TYPE_INSTALLED = 'installed'
29 |
30 | VALID_CLIENT = {
31 | TYPE_WEB: {
32 | 'required': [
33 | 'client_id',
34 | 'client_secret',
35 | 'redirect_uris',
36 | 'auth_uri',
37 | 'token_uri',
38 | ],
39 | 'string': [
40 | 'client_id',
41 | 'client_secret',
42 | ],
43 | },
44 | TYPE_INSTALLED: {
45 | 'required': [
46 | 'client_id',
47 | 'client_secret',
48 | 'redirect_uris',
49 | 'auth_uri',
50 | 'token_uri',
51 | ],
52 | 'string': [
53 | 'client_id',
54 | 'client_secret',
55 | ],
56 | },
57 | }
58 |
59 |
60 | class Error(Exception):
61 | """Base error for this module."""
62 | pass
63 |
64 |
65 | class InvalidClientSecretsError(Error):
66 | """Format of ClientSecrets file is invalid."""
67 | pass
68 |
69 |
70 | def _validate_clientsecrets(obj):
71 | if obj is None or len(obj) != 1:
72 | raise InvalidClientSecretsError('Invalid file format.')
73 | client_type = obj.keys()[0]
74 | if client_type not in VALID_CLIENT.keys():
75 | raise InvalidClientSecretsError('Unknown client type: %s.' % client_type)
76 | client_info = obj[client_type]
77 | for prop_name in VALID_CLIENT[client_type]['required']:
78 | if prop_name not in client_info:
79 | raise InvalidClientSecretsError(
80 | 'Missing property "%s" in a client type of "%s".' % (prop_name,
81 | client_type))
82 | for prop_name in VALID_CLIENT[client_type]['string']:
83 | if client_info[prop_name].startswith('[['):
84 | raise InvalidClientSecretsError(
85 | 'Property "%s" is not configured.' % prop_name)
86 | return client_type, client_info
87 |
88 |
89 | def load(fp):
90 | obj = simplejson.load(fp)
91 | return _validate_clientsecrets(obj)
92 |
93 |
94 | def loads(s):
95 | obj = simplejson.loads(s)
96 | return _validate_clientsecrets(obj)
97 |
98 |
99 | def _loadfile(filename):
100 | try:
101 | fp = file(filename, 'r')
102 | try:
103 | obj = simplejson.load(fp)
104 | finally:
105 | fp.close()
106 | except IOError:
107 | raise InvalidClientSecretsError('File not found: "%s"' % filename)
108 | return _validate_clientsecrets(obj)
109 |
110 |
111 | def loadfile(filename, cache=None):
112 | """Loading of client_secrets JSON file, optionally backed by a cache.
113 |
114 | Typical cache storage would be App Engine memcache service,
115 | but you can pass in any other cache client that implements
116 | these methods:
117 | - get(key, namespace=ns)
118 | - set(key, value, namespace=ns)
119 |
120 | Usage:
121 | # without caching
122 | client_type, client_info = loadfile('secrets.json')
123 | # using App Engine memcache service
124 | from google.appengine.api import memcache
125 | client_type, client_info = loadfile('secrets.json', cache=memcache)
126 |
127 | Args:
128 | filename: string, Path to a client_secrets.json file on a filesystem.
129 | cache: An optional cache service client that implements get() and set()
130 | methods. If not specified, the file is always being loaded from
131 | a filesystem.
132 |
133 | Raises:
134 | InvalidClientSecretsError: In case of a validation error or some
135 | I/O failure. Can happen only on cache miss.
136 |
137 | Returns:
138 | (client_type, client_info) tuple, as _loadfile() normally would.
139 | JSON contents is validated only during first load. Cache hits are not
140 | validated.
141 | """
142 | _SECRET_NAMESPACE = 'oauth2client:secrets#ns'
143 |
144 | if not cache:
145 | return _loadfile(filename)
146 |
147 | obj = cache.get(filename, namespace=_SECRET_NAMESPACE)
148 | if obj is None:
149 | client_type, client_info = _loadfile(filename)
150 | obj = {client_type: client_info}
151 | cache.set(filename, obj, namespace=_SECRET_NAMESPACE)
152 |
153 | return obj.iteritems().next()
154 |
--------------------------------------------------------------------------------
/uritemplate/__init__.py:
--------------------------------------------------------------------------------
1 | # Early, and incomplete implementation of -04.
2 | #
3 | import re
4 | import urllib
5 |
6 | RESERVED = ":/?#[]@!$&'()*+,;="
7 | OPERATOR = "+./;?|!@"
8 | EXPLODE = "*+"
9 | MODIFIER = ":^"
10 | TEMPLATE = re.compile(r"{(?P[\+\./;\?|!@])?(?P[^}]+)}", re.UNICODE)
11 | VAR = re.compile(r"^(?P[^=\+\*:\^]+)((?P[\+\*])|(?P[:\^]-?[0-9]+))?(=(?P.*))?$", re.UNICODE)
12 |
13 | def _tostring(varname, value, explode, operator, safe=""):
14 | if type(value) == type([]):
15 | if explode == "+":
16 | return ",".join([varname + "." + urllib.quote(x, safe) for x in value])
17 | else:
18 | return ",".join([urllib.quote(x, safe) for x in value])
19 | if type(value) == type({}):
20 | keys = value.keys()
21 | keys.sort()
22 | if explode == "+":
23 | return ",".join([varname + "." + urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
24 | else:
25 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
26 | else:
27 | return urllib.quote(value, safe)
28 |
29 |
30 | def _tostring_path(varname, value, explode, operator, safe=""):
31 | joiner = operator
32 | if type(value) == type([]):
33 | if explode == "+":
34 | return joiner.join([varname + "." + urllib.quote(x, safe) for x in value])
35 | elif explode == "*":
36 | return joiner.join([urllib.quote(x, safe) for x in value])
37 | else:
38 | return ",".join([urllib.quote(x, safe) for x in value])
39 | elif type(value) == type({}):
40 | keys = value.keys()
41 | keys.sort()
42 | if explode == "+":
43 | return joiner.join([varname + "." + urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
44 | elif explode == "*":
45 | return joiner.join([urllib.quote(key, safe) + joiner + urllib.quote(value[key], safe) for key in keys])
46 | else:
47 | return ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
48 | else:
49 | if value:
50 | return urllib.quote(value, safe)
51 | else:
52 | return ""
53 |
54 | def _tostring_query(varname, value, explode, operator, safe=""):
55 | joiner = operator
56 | varprefix = ""
57 | if operator == "?":
58 | joiner = "&"
59 | varprefix = varname + "="
60 | if type(value) == type([]):
61 | if 0 == len(value):
62 | return ""
63 | if explode == "+":
64 | return joiner.join([varname + "=" + urllib.quote(x, safe) for x in value])
65 | elif explode == "*":
66 | return joiner.join([urllib.quote(x, safe) for x in value])
67 | else:
68 | return varprefix + ",".join([urllib.quote(x, safe) for x in value])
69 | elif type(value) == type({}):
70 | if 0 == len(value):
71 | return ""
72 | keys = value.keys()
73 | keys.sort()
74 | if explode == "+":
75 | return joiner.join([varname + "." + urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
76 | elif explode == "*":
77 | return joiner.join([urllib.quote(key, safe) + "=" + urllib.quote(value[key], safe) for key in keys])
78 | else:
79 | return varprefix + ",".join([urllib.quote(key, safe) + "," + urllib.quote(value[key], safe) for key in keys])
80 | else:
81 | if value:
82 | return varname + "=" + urllib.quote(value, safe)
83 | else:
84 | return varname
85 |
86 | TOSTRING = {
87 | "" : _tostring,
88 | "+": _tostring,
89 | ";": _tostring_query,
90 | "?": _tostring_query,
91 | "/": _tostring_path,
92 | ".": _tostring_path,
93 | }
94 |
95 |
96 | def expand(template, vars):
97 | def _sub(match):
98 | groupdict = match.groupdict()
99 | operator = groupdict.get('operator')
100 | if operator is None:
101 | operator = ''
102 | varlist = groupdict.get('varlist')
103 |
104 | safe = "@"
105 | if operator == '+':
106 | safe = RESERVED
107 | varspecs = varlist.split(",")
108 | varnames = []
109 | defaults = {}
110 | for varspec in varspecs:
111 | m = VAR.search(varspec)
112 | groupdict = m.groupdict()
113 | varname = groupdict.get('varname')
114 | explode = groupdict.get('explode')
115 | partial = groupdict.get('partial')
116 | default = groupdict.get('default')
117 | if default:
118 | defaults[varname] = default
119 | varnames.append((varname, explode, partial))
120 |
121 | retval = []
122 | joiner = operator
123 | prefix = operator
124 | if operator == "+":
125 | prefix = ""
126 | joiner = ","
127 | if operator == "?":
128 | joiner = "&"
129 | if operator == "":
130 | joiner = ","
131 | for varname, explode, partial in varnames:
132 | if varname in vars:
133 | value = vars[varname]
134 | #if not value and (type(value) == type({}) or type(value) == type([])) and varname in defaults:
135 | if not value and value != "" and varname in defaults:
136 | value = defaults[varname]
137 | elif varname in defaults:
138 | value = defaults[varname]
139 | else:
140 | continue
141 | retval.append(TOSTRING[operator](varname, value, explode, operator, safe=safe))
142 | if "".join(retval):
143 | return prefix + joiner.join(retval)
144 | else:
145 | return ""
146 |
147 | return TEMPLATE.sub(_sub, template)
148 |
--------------------------------------------------------------------------------
/oauth2client/old_run.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2013 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """This module holds the old run() function which is deprecated, the
16 | tools.run_flow() function should be used in its place."""
17 |
18 |
19 | import logging
20 | import socket
21 | import sys
22 | import webbrowser
23 |
24 | import gflags
25 |
26 | from oauth2client import client
27 | from oauth2client import util
28 | from tools import ClientRedirectHandler
29 | from tools import ClientRedirectServer
30 |
31 |
32 | FLAGS = gflags.FLAGS
33 |
34 | gflags.DEFINE_boolean('auth_local_webserver', True,
35 | ('Run a local web server to handle redirects during '
36 | 'OAuth authorization.'))
37 |
38 | gflags.DEFINE_string('auth_host_name', 'localhost',
39 | ('Host name to use when running a local web server to '
40 | 'handle redirects during OAuth authorization.'))
41 |
42 | gflags.DEFINE_multi_int('auth_host_port', [8080, 8090],
43 | ('Port to use when running a local web server to '
44 | 'handle redirects during OAuth authorization.'))
45 |
46 |
47 | @util.positional(2)
48 | def run(flow, storage, http=None):
49 | """Core code for a command-line application.
50 |
51 | The run() function is called from your application and runs through all
52 | the steps to obtain credentials. It takes a Flow argument and attempts to
53 | open an authorization server page in the user's default web browser. The
54 | server asks the user to grant your application access to the user's data.
55 | If the user grants access, the run() function returns new credentials. The
56 | new credentials are also stored in the Storage argument, which updates the
57 | file associated with the Storage object.
58 |
59 | It presumes it is run from a command-line application and supports the
60 | following flags:
61 |
62 | --auth_host_name: Host name to use when running a local web server
63 | to handle redirects during OAuth authorization.
64 | (default: 'localhost')
65 |
66 | --auth_host_port: Port to use when running a local web server to handle
67 | redirects during OAuth authorization.;
68 | repeat this option to specify a list of values
69 | (default: '[8080, 8090]')
70 | (an integer)
71 |
72 | --[no]auth_local_webserver: Run a local web server to handle redirects
73 | during OAuth authorization.
74 | (default: 'true')
75 |
76 | Since it uses flags make sure to initialize the gflags module before
77 | calling run().
78 |
79 | Args:
80 | flow: Flow, an OAuth 2.0 Flow to step through.
81 | storage: Storage, a Storage to store the credential in.
82 | http: An instance of httplib2.Http.request
83 | or something that acts like it.
84 |
85 | Returns:
86 | Credentials, the obtained credential.
87 | """
88 | logging.warning('This function, oauth2client.tools.run(), and the use of '
89 | 'the gflags library are deprecated and will be removed in a future '
90 | 'version of the library.')
91 | if FLAGS.auth_local_webserver:
92 | success = False
93 | port_number = 0
94 | for port in FLAGS.auth_host_port:
95 | port_number = port
96 | try:
97 | httpd = ClientRedirectServer((FLAGS.auth_host_name, port),
98 | ClientRedirectHandler)
99 | except socket.error, e:
100 | pass
101 | else:
102 | success = True
103 | break
104 | FLAGS.auth_local_webserver = success
105 | if not success:
106 | print 'Failed to start a local webserver listening on either port 8080'
107 | print 'or port 9090. Please check your firewall settings and locally'
108 | print 'running programs that may be blocking or using those ports.'
109 | print
110 | print 'Falling back to --noauth_local_webserver and continuing with',
111 | print 'authorization.'
112 | print
113 |
114 | if FLAGS.auth_local_webserver:
115 | oauth_callback = 'http://%s:%s/' % (FLAGS.auth_host_name, port_number)
116 | else:
117 | oauth_callback = client.OOB_CALLBACK_URN
118 | flow.redirect_uri = oauth_callback
119 | authorize_url = flow.step1_get_authorize_url()
120 |
121 | if FLAGS.auth_local_webserver:
122 | webbrowser.open(authorize_url, new=1, autoraise=True)
123 | print 'Your browser has been opened to visit:'
124 | print
125 | print ' ' + authorize_url
126 | print
127 | print 'If your browser is on a different machine then exit and re-run'
128 | print 'this application with the command-line parameter '
129 | print
130 | print ' --noauth_local_webserver'
131 | print
132 | else:
133 | print 'Go to the following link in your browser:'
134 | print
135 | print ' ' + authorize_url
136 | print
137 |
138 | code = None
139 | if FLAGS.auth_local_webserver:
140 | httpd.handle_request()
141 | if 'error' in httpd.query_params:
142 | sys.exit('Authentication request was rejected.')
143 | if 'code' in httpd.query_params:
144 | code = httpd.query_params['code']
145 | else:
146 | print 'Failed to find "code" in the query parameters of the redirect.'
147 | sys.exit('Try running with --noauth_local_webserver.')
148 | else:
149 | code = raw_input('Enter verification code: ').strip()
150 |
151 | try:
152 | credential = flow.step2_exchange(code, http=http)
153 | except client.FlowExchangeError, e:
154 | sys.exit('Authentication has failed: %s' % e)
155 |
156 | storage.put(credential)
157 | credential.set_store(storage)
158 | print 'Authentication successful.'
159 |
160 | return credential
161 |
--------------------------------------------------------------------------------
/oauth2client/util.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2010 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 |
18 | """Common utility library."""
19 |
20 | __author__ = ['rafek@google.com (Rafe Kaplan)',
21 | 'guido@google.com (Guido van Rossum)',
22 | ]
23 | __all__ = [
24 | 'positional',
25 | 'POSITIONAL_WARNING',
26 | 'POSITIONAL_EXCEPTION',
27 | 'POSITIONAL_IGNORE',
28 | ]
29 |
30 | import inspect
31 | import logging
32 | import types
33 | import urllib
34 | import urlparse
35 |
36 | try:
37 | from urlparse import parse_qsl
38 | except ImportError:
39 | from cgi import parse_qsl
40 |
41 | logger = logging.getLogger(__name__)
42 |
43 | POSITIONAL_WARNING = 'WARNING'
44 | POSITIONAL_EXCEPTION = 'EXCEPTION'
45 | POSITIONAL_IGNORE = 'IGNORE'
46 | POSITIONAL_SET = frozenset([POSITIONAL_WARNING, POSITIONAL_EXCEPTION,
47 | POSITIONAL_IGNORE])
48 |
49 | positional_parameters_enforcement = POSITIONAL_WARNING
50 |
51 | def positional(max_positional_args):
52 | """A decorator to declare that only the first N arguments my be positional.
53 |
54 | This decorator makes it easy to support Python 3 style key-word only
55 | parameters. For example, in Python 3 it is possible to write:
56 |
57 | def fn(pos1, *, kwonly1=None, kwonly1=None):
58 | ...
59 |
60 | All named parameters after * must be a keyword:
61 |
62 | fn(10, 'kw1', 'kw2') # Raises exception.
63 | fn(10, kwonly1='kw1') # Ok.
64 |
65 | Example:
66 | To define a function like above, do:
67 |
68 | @positional(1)
69 | def fn(pos1, kwonly1=None, kwonly2=None):
70 | ...
71 |
72 | If no default value is provided to a keyword argument, it becomes a required
73 | keyword argument:
74 |
75 | @positional(0)
76 | def fn(required_kw):
77 | ...
78 |
79 | This must be called with the keyword parameter:
80 |
81 | fn() # Raises exception.
82 | fn(10) # Raises exception.
83 | fn(required_kw=10) # Ok.
84 |
85 | When defining instance or class methods always remember to account for
86 | 'self' and 'cls':
87 |
88 | class MyClass(object):
89 |
90 | @positional(2)
91 | def my_method(self, pos1, kwonly1=None):
92 | ...
93 |
94 | @classmethod
95 | @positional(2)
96 | def my_method(cls, pos1, kwonly1=None):
97 | ...
98 |
99 | The positional decorator behavior is controlled by
100 | util.positional_parameters_enforcement, which may be set to
101 | POSITIONAL_EXCEPTION, POSITIONAL_WARNING or POSITIONAL_IGNORE to raise an
102 | exception, log a warning, or do nothing, respectively, if a declaration is
103 | violated.
104 |
105 | Args:
106 | max_positional_arguments: Maximum number of positional arguments. All
107 | parameters after the this index must be keyword only.
108 |
109 | Returns:
110 | A decorator that prevents using arguments after max_positional_args from
111 | being used as positional parameters.
112 |
113 | Raises:
114 | TypeError if a key-word only argument is provided as a positional
115 | parameter, but only if util.positional_parameters_enforcement is set to
116 | POSITIONAL_EXCEPTION.
117 | """
118 | def positional_decorator(wrapped):
119 | def positional_wrapper(*args, **kwargs):
120 | if len(args) > max_positional_args:
121 | plural_s = ''
122 | if max_positional_args != 1:
123 | plural_s = 's'
124 | message = '%s() takes at most %d positional argument%s (%d given)' % (
125 | wrapped.__name__, max_positional_args, plural_s, len(args))
126 | if positional_parameters_enforcement == POSITIONAL_EXCEPTION:
127 | raise TypeError(message)
128 | elif positional_parameters_enforcement == POSITIONAL_WARNING:
129 | logger.warning(message)
130 | else: # IGNORE
131 | pass
132 | return wrapped(*args, **kwargs)
133 | return positional_wrapper
134 |
135 | if isinstance(max_positional_args, (int, long)):
136 | return positional_decorator
137 | else:
138 | args, _, _, defaults = inspect.getargspec(max_positional_args)
139 | return positional(len(args) - len(defaults))(max_positional_args)
140 |
141 |
142 | def scopes_to_string(scopes):
143 | """Converts scope value to a string.
144 |
145 | If scopes is a string then it is simply passed through. If scopes is an
146 | iterable then a string is returned that is all the individual scopes
147 | concatenated with spaces.
148 |
149 | Args:
150 | scopes: string or iterable of strings, the scopes.
151 |
152 | Returns:
153 | The scopes formatted as a single string.
154 | """
155 | if isinstance(scopes, types.StringTypes):
156 | return scopes
157 | else:
158 | return ' '.join(scopes)
159 |
160 |
161 | def dict_to_tuple_key(dictionary):
162 | """Converts a dictionary to a tuple that can be used as an immutable key.
163 |
164 | The resulting key is always sorted so that logically equivalent dictionaries
165 | always produce an identical tuple for a key.
166 |
167 | Args:
168 | dictionary: the dictionary to use as the key.
169 |
170 | Returns:
171 | A tuple representing the dictionary in it's naturally sorted ordering.
172 | """
173 | return tuple(sorted(dictionary.items()))
174 |
175 |
176 | def _add_query_parameter(url, name, value):
177 | """Adds a query parameter to a url.
178 |
179 | Replaces the current value if it already exists in the URL.
180 |
181 | Args:
182 | url: string, url to add the query parameter to.
183 | name: string, query parameter name.
184 | value: string, query parameter value.
185 |
186 | Returns:
187 | Updated query parameter. Does not update the url if value is None.
188 | """
189 | if value is None:
190 | return url
191 | else:
192 | parsed = list(urlparse.urlparse(url))
193 | q = dict(parse_qsl(parsed[4]))
194 | q[name] = value
195 | parsed[4] = urllib.urlencode(q)
196 | return urlparse.urlunparse(parsed)
197 |
--------------------------------------------------------------------------------
/apiclient/mimeparse.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2007 Joe Gregorio
2 | #
3 | # Licensed under the MIT License
4 |
5 | """MIME-Type Parser
6 |
7 | This module provides basic functions for handling mime-types. It can handle
8 | matching mime-types against a list of media-ranges. See section 14.1 of the
9 | HTTP specification [RFC 2616] for a complete explanation.
10 |
11 | http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
12 |
13 | Contents:
14 | - parse_mime_type(): Parses a mime-type into its component parts.
15 | - parse_media_range(): Media-ranges are mime-types with wild-cards and a 'q'
16 | quality parameter.
17 | - quality(): Determines the quality ('q') of a mime-type when
18 | compared against a list of media-ranges.
19 | - quality_parsed(): Just like quality() except the second parameter must be
20 | pre-parsed.
21 | - best_match(): Choose the mime-type with the highest quality ('q')
22 | from a list of candidates.
23 | """
24 |
25 | __version__ = '0.1.3'
26 | __author__ = 'Joe Gregorio'
27 | __email__ = 'joe@bitworking.org'
28 | __license__ = 'MIT License'
29 | __credits__ = ''
30 |
31 |
32 | def parse_mime_type(mime_type):
33 | """Parses a mime-type into its component parts.
34 |
35 | Carves up a mime-type and returns a tuple of the (type, subtype, params)
36 | where 'params' is a dictionary of all the parameters for the media range.
37 | For example, the media range 'application/xhtml;q=0.5' would get parsed
38 | into:
39 |
40 | ('application', 'xhtml', {'q', '0.5'})
41 | """
42 | parts = mime_type.split(';')
43 | params = dict([tuple([s.strip() for s in param.split('=', 1)])\
44 | for param in parts[1:]
45 | ])
46 | full_type = parts[0].strip()
47 | # Java URLConnection class sends an Accept header that includes a
48 | # single '*'. Turn it into a legal wildcard.
49 | if full_type == '*':
50 | full_type = '*/*'
51 | (type, subtype) = full_type.split('/')
52 |
53 | return (type.strip(), subtype.strip(), params)
54 |
55 |
56 | def parse_media_range(range):
57 | """Parse a media-range into its component parts.
58 |
59 | Carves up a media range and returns a tuple of the (type, subtype,
60 | params) where 'params' is a dictionary of all the parameters for the media
61 | range. For example, the media range 'application/*;q=0.5' would get parsed
62 | into:
63 |
64 | ('application', '*', {'q', '0.5'})
65 |
66 | In addition this function also guarantees that there is a value for 'q'
67 | in the params dictionary, filling it in with a proper default if
68 | necessary.
69 | """
70 | (type, subtype, params) = parse_mime_type(range)
71 | if not params.has_key('q') or not params['q'] or \
72 | not float(params['q']) or float(params['q']) > 1\
73 | or float(params['q']) < 0:
74 | params['q'] = '1'
75 |
76 | return (type, subtype, params)
77 |
78 |
79 | def fitness_and_quality_parsed(mime_type, parsed_ranges):
80 | """Find the best match for a mime-type amongst parsed media-ranges.
81 |
82 | Find the best match for a given mime-type against a list of media_ranges
83 | that have already been parsed by parse_media_range(). Returns a tuple of
84 | the fitness value and the value of the 'q' quality parameter of the best
85 | match, or (-1, 0) if no match was found. Just as for quality_parsed(),
86 | 'parsed_ranges' must be a list of parsed media ranges.
87 | """
88 | best_fitness = -1
89 | best_fit_q = 0
90 | (target_type, target_subtype, target_params) =\
91 | parse_media_range(mime_type)
92 | for (type, subtype, params) in parsed_ranges:
93 | type_match = (type == target_type or\
94 | type == '*' or\
95 | target_type == '*')
96 | subtype_match = (subtype == target_subtype or\
97 | subtype == '*' or\
98 | target_subtype == '*')
99 | if type_match and subtype_match:
100 | param_matches = reduce(lambda x, y: x + y, [1 for (key, value) in \
101 | target_params.iteritems() if key != 'q' and \
102 | params.has_key(key) and value == params[key]], 0)
103 | fitness = (type == target_type) and 100 or 0
104 | fitness += (subtype == target_subtype) and 10 or 0
105 | fitness += param_matches
106 | if fitness > best_fitness:
107 | best_fitness = fitness
108 | best_fit_q = params['q']
109 |
110 | return best_fitness, float(best_fit_q)
111 |
112 |
113 | def quality_parsed(mime_type, parsed_ranges):
114 | """Find the best match for a mime-type amongst parsed media-ranges.
115 |
116 | Find the best match for a given mime-type against a list of media_ranges
117 | that have already been parsed by parse_media_range(). Returns the 'q'
118 | quality parameter of the best match, 0 if no match was found. This function
119 | bahaves the same as quality() except that 'parsed_ranges' must be a list of
120 | parsed media ranges.
121 | """
122 |
123 | return fitness_and_quality_parsed(mime_type, parsed_ranges)[1]
124 |
125 |
126 | def quality(mime_type, ranges):
127 | """Return the quality ('q') of a mime-type against a list of media-ranges.
128 |
129 | Returns the quality 'q' of a mime-type when compared against the
130 | media-ranges in ranges. For example:
131 |
132 | >>> quality('text/html','text/*;q=0.3, text/html;q=0.7,
133 | text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5')
134 | 0.7
135 |
136 | """
137 | parsed_ranges = [parse_media_range(r) for r in ranges.split(',')]
138 |
139 | return quality_parsed(mime_type, parsed_ranges)
140 |
141 |
142 | def best_match(supported, header):
143 | """Return mime-type with the highest quality ('q') from list of candidates.
144 |
145 | Takes a list of supported mime-types and finds the best match for all the
146 | media-ranges listed in header. The value of header must be a string that
147 | conforms to the format of the HTTP Accept: header. The value of 'supported'
148 | is a list of mime-types. The list of supported mime-types should be sorted
149 | in order of increasing desirability, in case of a situation where there is
150 | a tie.
151 |
152 | >>> best_match(['application/xbel+xml', 'text/xml'],
153 | 'text/*;q=0.5,*/*; q=0.1')
154 | 'text/xml'
155 | """
156 | split_header = _filter_blank(header.split(','))
157 | parsed_header = [parse_media_range(r) for r in split_header]
158 | weighted_matches = []
159 | pos = 0
160 | for mime_type in supported:
161 | weighted_matches.append((fitness_and_quality_parsed(mime_type,
162 | parsed_header), pos, mime_type))
163 | pos += 1
164 | weighted_matches.sort()
165 |
166 | return weighted_matches[-1][0][1] and weighted_matches[-1][2] or ''
167 |
168 |
169 | def _filter_blank(i):
170 | for s in i:
171 | if s.strip():
172 | yield s
173 |
--------------------------------------------------------------------------------
/oauth2client/tools.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2013 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Command-line tools for authenticating via OAuth 2.0
16 |
17 | Do the OAuth 2.0 Web Server dance for a command line application. Stores the
18 | generated credentials in a common file that is used by other example apps in
19 | the same directory.
20 | """
21 |
22 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
23 | __all__ = ['argparser', 'run_flow', 'run', 'message_if_missing']
24 |
25 |
26 | import BaseHTTPServer
27 | import argparse
28 | import httplib2
29 | import logging
30 | import os
31 | import socket
32 | import sys
33 | import webbrowser
34 |
35 | from oauth2client import client
36 | from oauth2client import file
37 | from oauth2client import util
38 |
39 | try:
40 | from urlparse import parse_qsl
41 | except ImportError:
42 | from cgi import parse_qsl
43 |
44 | _CLIENT_SECRETS_MESSAGE = """WARNING: Please configure OAuth 2.0
45 |
46 | To make this sample run you will need to populate the client_secrets.json file
47 | found at:
48 |
49 | %s
50 |
51 | with information from the APIs Console .
52 |
53 | """
54 |
55 | # run_parser is an ArgumentParser that contains command-line options expected
56 | # by tools.run(). Pass it in as part of the 'parents' argument to your own
57 | # ArgumentParser.
58 | argparser = argparse.ArgumentParser(add_help=False)
59 | argparser.add_argument('--auth_host_name', default='localhost',
60 | help='Hostname when running a local web server.')
61 | argparser.add_argument('--noauth_local_webserver', action='store_true',
62 | default=False, help='Do not run a local web server.')
63 | argparser.add_argument('--auth_host_port', default=[8080, 8090], type=int,
64 | nargs='*', help='Port web server should listen on.')
65 | argparser.add_argument('--logging_level', default='ERROR',
66 | choices=['DEBUG', 'INFO', 'WARNING', 'ERROR',
67 | 'CRITICAL'],
68 | help='Set the logging level of detail.')
69 |
70 |
71 | class ClientRedirectServer(BaseHTTPServer.HTTPServer):
72 | """A server to handle OAuth 2.0 redirects back to localhost.
73 |
74 | Waits for a single request and parses the query parameters
75 | into query_params and then stops serving.
76 | """
77 | query_params = {}
78 |
79 |
80 | class ClientRedirectHandler(BaseHTTPServer.BaseHTTPRequestHandler):
81 | """A handler for OAuth 2.0 redirects back to localhost.
82 |
83 | Waits for a single request and parses the query parameters
84 | into the servers query_params and then stops serving.
85 | """
86 |
87 | def do_GET(s):
88 | """Handle a GET request.
89 |
90 | Parses the query parameters and prints a message
91 | if the flow has completed. Note that we can't detect
92 | if an error occurred.
93 | """
94 | s.send_response(200)
95 | s.send_header("Content-type", "text/html")
96 | s.end_headers()
97 | query = s.path.split('?', 1)[-1]
98 | query = dict(parse_qsl(query))
99 | s.server.query_params = query
100 | s.wfile.write("Authentication Status")
101 | s.wfile.write("
The authentication flow has completed.
")
102 | s.wfile.write("")
103 |
104 | def log_message(self, format, *args):
105 | """Do not log messages to stdout while running as command line program."""
106 | pass
107 |
108 |
109 | @util.positional(3)
110 | def run_flow(flow, storage, flags, http=None):
111 | """Core code for a command-line application.
112 |
113 | The run() function is called from your application and runs through all the
114 | steps to obtain credentials. It takes a Flow argument and attempts to open an
115 | authorization server page in the user's default web browser. The server asks
116 | the user to grant your application access to the user's data. If the user
117 | grants access, the run() function returns new credentials. The new credentials
118 | are also stored in the Storage argument, which updates the file associated
119 | with the Storage object.
120 |
121 | It presumes it is run from a command-line application and supports the
122 | following flags:
123 |
124 | --auth_host_name: Host name to use when running a local web server
125 | to handle redirects during OAuth authorization.
126 | (default: 'localhost')
127 |
128 | --auth_host_port: Port to use when running a local web server to handle
129 | redirects during OAuth authorization.;
130 | repeat this option to specify a list of values
131 | (default: '[8080, 8090]')
132 | (an integer)
133 |
134 | --[no]auth_local_webserver: Run a local web server to handle redirects
135 | during OAuth authorization.
136 | (default: 'true')
137 |
138 | The tools module defines an ArgumentParser the already contains the flag
139 | definitions that run() requires. You can pass that ArgumentParser to your
140 | ArgumentParser constructor:
141 |
142 | parser = argparse.ArgumentParser(description=__doc__,
143 | formatter_class=argparse.RawDescriptionHelpFormatter,
144 | parents=[tools.run_parser])
145 | flags = parser.parse_args(argv)
146 |
147 | Args:
148 | flow: Flow, an OAuth 2.0 Flow to step through.
149 | storage: Storage, a Storage to store the credential in.
150 | flags: argparse.ArgumentParser, the command-line flags.
151 | http: An instance of httplib2.Http.request
152 | or something that acts like it.
153 |
154 | Returns:
155 | Credentials, the obtained credential.
156 | """
157 | logging.getLogger().setLevel(getattr(logging, flags.logging_level))
158 | if not flags.noauth_local_webserver:
159 | success = False
160 | port_number = 0
161 | for port in flags.auth_host_port:
162 | port_number = port
163 | try:
164 | httpd = ClientRedirectServer((flags.auth_host_name, port),
165 | ClientRedirectHandler)
166 | except socket.error, e:
167 | pass
168 | else:
169 | success = True
170 | break
171 | flags.noauth_local_webserver = not success
172 | if not success:
173 | print 'Failed to start a local webserver listening on either port 8080'
174 | print 'or port 9090. Please check your firewall settings and locally'
175 | print 'running programs that may be blocking or using those ports.'
176 | print
177 | print 'Falling back to --noauth_local_webserver and continuing with',
178 | print 'authorization.'
179 | print
180 |
181 | if not flags.noauth_local_webserver:
182 | oauth_callback = 'http://%s:%s/' % (flags.auth_host_name, port_number)
183 | else:
184 | oauth_callback = client.OOB_CALLBACK_URN
185 | flow.redirect_uri = oauth_callback
186 | authorize_url = flow.step1_get_authorize_url()
187 |
188 | if not flags.noauth_local_webserver:
189 | webbrowser.open(authorize_url, new=1, autoraise=True)
190 | print 'Your browser has been opened to visit:'
191 | print
192 | print ' ' + authorize_url
193 | print
194 | print 'If your browser is on a different machine then exit and re-run this'
195 | print 'application with the command-line parameter '
196 | print
197 | print ' --noauth_local_webserver'
198 | print
199 | else:
200 | print 'Go to the following link in your browser:'
201 | print
202 | print ' ' + authorize_url
203 | print
204 |
205 | code = None
206 | if not flags.noauth_local_webserver:
207 | httpd.handle_request()
208 | if 'error' in httpd.query_params:
209 | sys.exit('Authentication request was rejected.')
210 | if 'code' in httpd.query_params:
211 | code = httpd.query_params['code']
212 | else:
213 | print 'Failed to find "code" in the query parameters of the redirect.'
214 | sys.exit('Try running with --noauth_local_webserver.')
215 | else:
216 | code = raw_input('Enter verification code: ').strip()
217 |
218 | try:
219 | credential = flow.step2_exchange(code, http=http)
220 | except client.FlowExchangeError, e:
221 | sys.exit('Authentication has failed: %s' % e)
222 |
223 | storage.put(credential)
224 | credential.set_store(storage)
225 | print 'Authentication successful.'
226 |
227 | return credential
228 |
229 |
230 | def message_if_missing(filename):
231 | """Helpful message to display if the CLIENT_SECRETS file is missing."""
232 |
233 | return _CLIENT_SECRETS_MESSAGE % filename
234 |
235 | try:
236 | from old_run import run
237 | from old_run import FLAGS
238 | except ImportError:
239 | def run(*args, **kwargs):
240 | raise NotImplementedError(
241 | 'The gflags library must be installed to use tools.run(). '
242 | 'Please install gflags or preferrably switch to using '
243 | 'tools.run_flow().')
244 |
--------------------------------------------------------------------------------
/apiclient/schema.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Schema processing for discovery based APIs
16 |
17 | Schemas holds an APIs discovery schemas. It can return those schema as
18 | deserialized JSON objects, or pretty print them as prototype objects that
19 | conform to the schema.
20 |
21 | For example, given the schema:
22 |
23 | schema = \"\"\"{
24 | "Foo": {
25 | "type": "object",
26 | "properties": {
27 | "etag": {
28 | "type": "string",
29 | "description": "ETag of the collection."
30 | },
31 | "kind": {
32 | "type": "string",
33 | "description": "Type of the collection ('calendar#acl').",
34 | "default": "calendar#acl"
35 | },
36 | "nextPageToken": {
37 | "type": "string",
38 | "description": "Token used to access the next
39 | page of this result. Omitted if no further results are available."
40 | }
41 | }
42 | }
43 | }\"\"\"
44 |
45 | s = Schemas(schema)
46 | print s.prettyPrintByName('Foo')
47 |
48 | Produces the following output:
49 |
50 | {
51 | "nextPageToken": "A String", # Token used to access the
52 | # next page of this result. Omitted if no further results are available.
53 | "kind": "A String", # Type of the collection ('calendar#acl').
54 | "etag": "A String", # ETag of the collection.
55 | },
56 |
57 | The constructor takes a discovery document in which to look up named schema.
58 | """
59 |
60 | # TODO(jcgregorio) support format, enum, minimum, maximum
61 |
62 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
63 |
64 | import copy
65 |
66 | from oauth2client import util
67 | from oauth2client.anyjson import simplejson
68 |
69 |
70 | class Schemas(object):
71 | """Schemas for an API."""
72 |
73 | def __init__(self, discovery):
74 | """Constructor.
75 |
76 | Args:
77 | discovery: object, Deserialized discovery document from which we pull
78 | out the named schema.
79 | """
80 | self.schemas = discovery.get('schemas', {})
81 |
82 | # Cache of pretty printed schemas.
83 | self.pretty = {}
84 |
85 | @util.positional(2)
86 | def _prettyPrintByName(self, name, seen=None, dent=0):
87 | """Get pretty printed object prototype from the schema name.
88 |
89 | Args:
90 | name: string, Name of schema in the discovery document.
91 | seen: list of string, Names of schema already seen. Used to handle
92 | recursive definitions.
93 |
94 | Returns:
95 | string, A string that contains a prototype object with
96 | comments that conforms to the given schema.
97 | """
98 | if seen is None:
99 | seen = []
100 |
101 | if name in seen:
102 | # Do not fall into an infinite loop over recursive definitions.
103 | return '# Object with schema name: %s' % name
104 | seen.append(name)
105 |
106 | if name not in self.pretty:
107 | self.pretty[name] = _SchemaToStruct(self.schemas[name],
108 | seen, dent=dent).to_str(self._prettyPrintByName)
109 |
110 | seen.pop()
111 |
112 | return self.pretty[name]
113 |
114 | def prettyPrintByName(self, name):
115 | """Get pretty printed object prototype from the schema name.
116 |
117 | Args:
118 | name: string, Name of schema in the discovery document.
119 |
120 | Returns:
121 | string, A string that contains a prototype object with
122 | comments that conforms to the given schema.
123 | """
124 | # Return with trailing comma and newline removed.
125 | return self._prettyPrintByName(name, seen=[], dent=1)[:-2]
126 |
127 | @util.positional(2)
128 | def _prettyPrintSchema(self, schema, seen=None, dent=0):
129 | """Get pretty printed object prototype of schema.
130 |
131 | Args:
132 | schema: object, Parsed JSON schema.
133 | seen: list of string, Names of schema already seen. Used to handle
134 | recursive definitions.
135 |
136 | Returns:
137 | string, A string that contains a prototype object with
138 | comments that conforms to the given schema.
139 | """
140 | if seen is None:
141 | seen = []
142 |
143 | return _SchemaToStruct(schema, seen, dent=dent).to_str(self._prettyPrintByName)
144 |
145 | def prettyPrintSchema(self, schema):
146 | """Get pretty printed object prototype of schema.
147 |
148 | Args:
149 | schema: object, Parsed JSON schema.
150 |
151 | Returns:
152 | string, A string that contains a prototype object with
153 | comments that conforms to the given schema.
154 | """
155 | # Return with trailing comma and newline removed.
156 | return self._prettyPrintSchema(schema, dent=1)[:-2]
157 |
158 | def get(self, name):
159 | """Get deserialized JSON schema from the schema name.
160 |
161 | Args:
162 | name: string, Schema name.
163 | """
164 | return self.schemas[name]
165 |
166 |
167 | class _SchemaToStruct(object):
168 | """Convert schema to a prototype object."""
169 |
170 | @util.positional(3)
171 | def __init__(self, schema, seen, dent=0):
172 | """Constructor.
173 |
174 | Args:
175 | schema: object, Parsed JSON schema.
176 | seen: list, List of names of schema already seen while parsing. Used to
177 | handle recursive definitions.
178 | dent: int, Initial indentation depth.
179 | """
180 | # The result of this parsing kept as list of strings.
181 | self.value = []
182 |
183 | # The final value of the parsing.
184 | self.string = None
185 |
186 | # The parsed JSON schema.
187 | self.schema = schema
188 |
189 | # Indentation level.
190 | self.dent = dent
191 |
192 | # Method that when called returns a prototype object for the schema with
193 | # the given name.
194 | self.from_cache = None
195 |
196 | # List of names of schema already seen while parsing.
197 | self.seen = seen
198 |
199 | def emit(self, text):
200 | """Add text as a line to the output.
201 |
202 | Args:
203 | text: string, Text to output.
204 | """
205 | self.value.extend([" " * self.dent, text, '\n'])
206 |
207 | def emitBegin(self, text):
208 | """Add text to the output, but with no line terminator.
209 |
210 | Args:
211 | text: string, Text to output.
212 | """
213 | self.value.extend([" " * self.dent, text])
214 |
215 | def emitEnd(self, text, comment):
216 | """Add text and comment to the output with line terminator.
217 |
218 | Args:
219 | text: string, Text to output.
220 | comment: string, Python comment.
221 | """
222 | if comment:
223 | divider = '\n' + ' ' * (self.dent + 2) + '# '
224 | lines = comment.splitlines()
225 | lines = [x.rstrip() for x in lines]
226 | comment = divider.join(lines)
227 | self.value.extend([text, ' # ', comment, '\n'])
228 | else:
229 | self.value.extend([text, '\n'])
230 |
231 | def indent(self):
232 | """Increase indentation level."""
233 | self.dent += 1
234 |
235 | def undent(self):
236 | """Decrease indentation level."""
237 | self.dent -= 1
238 |
239 | def _to_str_impl(self, schema):
240 | """Prototype object based on the schema, in Python code with comments.
241 |
242 | Args:
243 | schema: object, Parsed JSON schema file.
244 |
245 | Returns:
246 | Prototype object based on the schema, in Python code with comments.
247 | """
248 | stype = schema.get('type')
249 | if stype == 'object':
250 | self.emitEnd('{', schema.get('description', ''))
251 | self.indent()
252 | if 'properties' in schema:
253 | for pname, pschema in schema.get('properties', {}).iteritems():
254 | self.emitBegin('"%s": ' % pname)
255 | self._to_str_impl(pschema)
256 | elif 'additionalProperties' in schema:
257 | self.emitBegin('"a_key": ')
258 | self._to_str_impl(schema['additionalProperties'])
259 | self.undent()
260 | self.emit('},')
261 | elif '$ref' in schema:
262 | schemaName = schema['$ref']
263 | description = schema.get('description', '')
264 | s = self.from_cache(schemaName, seen=self.seen)
265 | parts = s.splitlines()
266 | self.emitEnd(parts[0], description)
267 | for line in parts[1:]:
268 | self.emit(line.rstrip())
269 | elif stype == 'boolean':
270 | value = schema.get('default', 'True or False')
271 | self.emitEnd('%s,' % str(value), schema.get('description', ''))
272 | elif stype == 'string':
273 | value = schema.get('default', 'A String')
274 | self.emitEnd('"%s",' % str(value), schema.get('description', ''))
275 | elif stype == 'integer':
276 | value = schema.get('default', '42')
277 | self.emitEnd('%s,' % str(value), schema.get('description', ''))
278 | elif stype == 'number':
279 | value = schema.get('default', '3.14')
280 | self.emitEnd('%s,' % str(value), schema.get('description', ''))
281 | elif stype == 'null':
282 | self.emitEnd('None,', schema.get('description', ''))
283 | elif stype == 'any':
284 | self.emitEnd('"",', schema.get('description', ''))
285 | elif stype == 'array':
286 | self.emitEnd('[', schema.get('description'))
287 | self.indent()
288 | self.emitBegin('')
289 | self._to_str_impl(schema['items'])
290 | self.undent()
291 | self.emit('],')
292 | else:
293 | self.emit('Unknown type! %s' % stype)
294 | self.emitEnd('', '')
295 |
296 | self.string = ''.join(self.value)
297 | return self.string
298 |
299 | def to_str(self, from_cache):
300 | """Prototype object based on the schema, in Python code with comments.
301 |
302 | Args:
303 | from_cache: callable(name, seen), Callable that retrieves an object
304 | prototype for a schema with the given name. Seen is a list of schema
305 | names already seen as we recursively descend the schema definition.
306 |
307 | Returns:
308 | Prototype object based on the schema, in Python code with comments.
309 | The lines of the code will all be properly indented.
310 | """
311 | self.from_cache = from_cache
312 | return self._to_str_impl(self.schema)
313 |
--------------------------------------------------------------------------------
/apiclient/channel.py:
--------------------------------------------------------------------------------
1 | """Channel notifications support.
2 |
3 | Classes and functions to support channel subscriptions and notifications
4 | on those channels.
5 |
6 | Notes:
7 | - This code is based on experimental APIs and is subject to change.
8 | - Notification does not do deduplication of notification ids, that's up to
9 | the receiver.
10 | - Storing the Channel between calls is up to the caller.
11 |
12 |
13 | Example setting up a channel:
14 |
15 | # Create a new channel that gets notifications via webhook.
16 | channel = new_webhook_channel("https://example.com/my_web_hook")
17 |
18 | # Store the channel, keyed by 'channel.id'. Store it before calling the
19 | # watch method because notifications may start arriving before the watch
20 | # method returns.
21 | ...
22 |
23 | resp = service.objects().watchAll(
24 | bucket="some_bucket_id", body=channel.body()).execute()
25 | channel.update(resp)
26 |
27 | # Store the channel, keyed by 'channel.id'. Store it after being updated
28 | # since the resource_id value will now be correct, and that's needed to
29 | # stop a subscription.
30 | ...
31 |
32 |
33 | An example Webhook implementation using webapp2. Note that webapp2 puts
34 | headers in a case insensitive dictionary, as headers aren't guaranteed to
35 | always be upper case.
36 |
37 | id = self.request.headers[X_GOOG_CHANNEL_ID]
38 |
39 | # Retrieve the channel by id.
40 | channel = ...
41 |
42 | # Parse notification from the headers, including validating the id.
43 | n = notification_from_headers(channel, self.request.headers)
44 |
45 | # Do app specific stuff with the notification here.
46 | if n.resource_state == 'sync':
47 | # Code to handle sync state.
48 | elif n.resource_state == 'exists':
49 | # Code to handle the exists state.
50 | elif n.resource_state == 'not_exists':
51 | # Code to handle the not exists state.
52 |
53 |
54 | Example of unsubscribing.
55 |
56 | service.channels().stop(channel.body())
57 | """
58 |
59 | import datetime
60 | import uuid
61 |
62 | from apiclient import errors
63 | from oauth2client import util
64 |
65 |
66 | # The unix time epoch starts at midnight 1970.
67 | EPOCH = datetime.datetime.utcfromtimestamp(0)
68 |
69 | # Map the names of the parameters in the JSON channel description to
70 | # the parameter names we use in the Channel class.
71 | CHANNEL_PARAMS = {
72 | 'address': 'address',
73 | 'id': 'id',
74 | 'expiration': 'expiration',
75 | 'params': 'params',
76 | 'resourceId': 'resource_id',
77 | 'resourceUri': 'resource_uri',
78 | 'type': 'type',
79 | 'token': 'token',
80 | }
81 |
82 | X_GOOG_CHANNEL_ID = 'X-GOOG-CHANNEL-ID'
83 | X_GOOG_MESSAGE_NUMBER = 'X-GOOG-MESSAGE-NUMBER'
84 | X_GOOG_RESOURCE_STATE = 'X-GOOG-RESOURCE-STATE'
85 | X_GOOG_RESOURCE_URI = 'X-GOOG-RESOURCE-URI'
86 | X_GOOG_RESOURCE_ID = 'X-GOOG-RESOURCE-ID'
87 |
88 |
89 | def _upper_header_keys(headers):
90 | new_headers = {}
91 | for k, v in headers.iteritems():
92 | new_headers[k.upper()] = v
93 | return new_headers
94 |
95 |
96 | class Notification(object):
97 | """A Notification from a Channel.
98 |
99 | Notifications are not usually constructed directly, but are returned
100 | from functions like notification_from_headers().
101 |
102 | Attributes:
103 | message_number: int, The unique id number of this notification.
104 | state: str, The state of the resource being monitored.
105 | uri: str, The address of the resource being monitored.
106 | resource_id: str, The unique identifier of the version of the resource at
107 | this event.
108 | """
109 | @util.positional(5)
110 | def __init__(self, message_number, state, resource_uri, resource_id):
111 | """Notification constructor.
112 |
113 | Args:
114 | message_number: int, The unique id number of this notification.
115 | state: str, The state of the resource being monitored. Can be one
116 | of "exists", "not_exists", or "sync".
117 | resource_uri: str, The address of the resource being monitored.
118 | resource_id: str, The identifier of the watched resource.
119 | """
120 | self.message_number = message_number
121 | self.state = state
122 | self.resource_uri = resource_uri
123 | self.resource_id = resource_id
124 |
125 |
126 | class Channel(object):
127 | """A Channel for notifications.
128 |
129 | Usually not constructed directly, instead it is returned from helper
130 | functions like new_webhook_channel().
131 |
132 | Attributes:
133 | type: str, The type of delivery mechanism used by this channel. For
134 | example, 'web_hook'.
135 | id: str, A UUID for the channel.
136 | token: str, An arbitrary string associated with the channel that
137 | is delivered to the target address with each event delivered
138 | over this channel.
139 | address: str, The address of the receiving entity where events are
140 | delivered. Specific to the channel type.
141 | expiration: int, The time, in milliseconds from the epoch, when this
142 | channel will expire.
143 | params: dict, A dictionary of string to string, with additional parameters
144 | controlling delivery channel behavior.
145 | resource_id: str, An opaque id that identifies the resource that is
146 | being watched. Stable across different API versions.
147 | resource_uri: str, The canonicalized ID of the watched resource.
148 | """
149 |
150 | @util.positional(5)
151 | def __init__(self, type, id, token, address, expiration=None,
152 | params=None, resource_id="", resource_uri=""):
153 | """Create a new Channel.
154 |
155 | In user code, this Channel constructor will not typically be called
156 | manually since there are functions for creating channels for each specific
157 | type with a more customized set of arguments to pass.
158 |
159 | Args:
160 | type: str, The type of delivery mechanism used by this channel. For
161 | example, 'web_hook'.
162 | id: str, A UUID for the channel.
163 | token: str, An arbitrary string associated with the channel that
164 | is delivered to the target address with each event delivered
165 | over this channel.
166 | address: str, The address of the receiving entity where events are
167 | delivered. Specific to the channel type.
168 | expiration: int, The time, in milliseconds from the epoch, when this
169 | channel will expire.
170 | params: dict, A dictionary of string to string, with additional parameters
171 | controlling delivery channel behavior.
172 | resource_id: str, An opaque id that identifies the resource that is
173 | being watched. Stable across different API versions.
174 | resource_uri: str, The canonicalized ID of the watched resource.
175 | """
176 | self.type = type
177 | self.id = id
178 | self.token = token
179 | self.address = address
180 | self.expiration = expiration
181 | self.params = params
182 | self.resource_id = resource_id
183 | self.resource_uri = resource_uri
184 |
185 | def body(self):
186 | """Build a body from the Channel.
187 |
188 | Constructs a dictionary that's appropriate for passing into watch()
189 | methods as the value of body argument.
190 |
191 | Returns:
192 | A dictionary representation of the channel.
193 | """
194 | result = {
195 | 'id': self.id,
196 | 'token': self.token,
197 | 'type': self.type,
198 | 'address': self.address
199 | }
200 | if self.params:
201 | result['params'] = self.params
202 | if self.resource_id:
203 | result['resourceId'] = self.resource_id
204 | if self.resource_uri:
205 | result['resourceUri'] = self.resource_uri
206 | if self.expiration:
207 | result['expiration'] = self.expiration
208 |
209 | return result
210 |
211 | def update(self, resp):
212 | """Update a channel with information from the response of watch().
213 |
214 | When a request is sent to watch() a resource, the response returned
215 | from the watch() request is a dictionary with updated channel information,
216 | such as the resource_id, which is needed when stopping a subscription.
217 |
218 | Args:
219 | resp: dict, The response from a watch() method.
220 | """
221 | for json_name, param_name in CHANNEL_PARAMS.iteritems():
222 | value = resp.get(json_name)
223 | if value is not None:
224 | setattr(self, param_name, value)
225 |
226 |
227 | def notification_from_headers(channel, headers):
228 | """Parse a notification from the webhook request headers, validate
229 | the notification, and return a Notification object.
230 |
231 | Args:
232 | channel: Channel, The channel that the notification is associated with.
233 | headers: dict, A dictionary like object that contains the request headers
234 | from the webhook HTTP request.
235 |
236 | Returns:
237 | A Notification object.
238 |
239 | Raises:
240 | errors.InvalidNotificationError if the notification is invalid.
241 | ValueError if the X-GOOG-MESSAGE-NUMBER can't be converted to an int.
242 | """
243 | headers = _upper_header_keys(headers)
244 | channel_id = headers[X_GOOG_CHANNEL_ID]
245 | if channel.id != channel_id:
246 | raise errors.InvalidNotificationError(
247 | 'Channel id mismatch: %s != %s' % (channel.id, channel_id))
248 | else:
249 | message_number = int(headers[X_GOOG_MESSAGE_NUMBER])
250 | state = headers[X_GOOG_RESOURCE_STATE]
251 | resource_uri = headers[X_GOOG_RESOURCE_URI]
252 | resource_id = headers[X_GOOG_RESOURCE_ID]
253 | return Notification(message_number, state, resource_uri, resource_id)
254 |
255 |
256 | @util.positional(2)
257 | def new_webhook_channel(url, token=None, expiration=None, params=None):
258 | """Create a new webhook Channel.
259 |
260 | Args:
261 | url: str, URL to post notifications to.
262 | token: str, An arbitrary string associated with the channel that
263 | is delivered to the target address with each notification delivered
264 | over this channel.
265 | expiration: datetime.datetime, A time in the future when the channel
266 | should expire. Can also be None if the subscription should use the
267 | default expiration. Note that different services may have different
268 | limits on how long a subscription lasts. Check the response from the
269 | watch() method to see the value the service has set for an expiration
270 | time.
271 | params: dict, Extra parameters to pass on channel creation. Currently
272 | not used for webhook channels.
273 | """
274 | expiration_ms = 0
275 | if expiration:
276 | delta = expiration - EPOCH
277 | expiration_ms = delta.microseconds/1000 + (
278 | delta.seconds + delta.days*24*3600)*1000
279 | if expiration_ms < 0:
280 | expiration_ms = 0
281 |
282 | return Channel('web_hook', str(uuid.uuid4()),
283 | token, url, expiration=expiration_ms,
284 | params=params)
285 |
286 |
--------------------------------------------------------------------------------
/oauth2client/crypt.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2.4
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Copyright (C) 2011 Google Inc.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | import base64
19 | import hashlib
20 | import logging
21 | import time
22 |
23 | from anyjson import simplejson
24 |
25 |
26 | CLOCK_SKEW_SECS = 300 # 5 minutes in seconds
27 | AUTH_TOKEN_LIFETIME_SECS = 300 # 5 minutes in seconds
28 | MAX_TOKEN_LIFETIME_SECS = 86400 # 1 day in seconds
29 |
30 |
31 | logger = logging.getLogger(__name__)
32 |
33 |
34 | class AppIdentityError(Exception):
35 | pass
36 |
37 |
38 | try:
39 | from OpenSSL import crypto
40 |
41 |
42 | class OpenSSLVerifier(object):
43 | """Verifies the signature on a message."""
44 |
45 | def __init__(self, pubkey):
46 | """Constructor.
47 |
48 | Args:
49 | pubkey, OpenSSL.crypto.PKey, The public key to verify with.
50 | """
51 | self._pubkey = pubkey
52 |
53 | def verify(self, message, signature):
54 | """Verifies a message against a signature.
55 |
56 | Args:
57 | message: string, The message to verify.
58 | signature: string, The signature on the message.
59 |
60 | Returns:
61 | True if message was signed by the private key associated with the public
62 | key that this object was constructed with.
63 | """
64 | try:
65 | crypto.verify(self._pubkey, signature, message, 'sha256')
66 | return True
67 | except:
68 | return False
69 |
70 | @staticmethod
71 | def from_string(key_pem, is_x509_cert):
72 | """Construct a Verified instance from a string.
73 |
74 | Args:
75 | key_pem: string, public key in PEM format.
76 | is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
77 | expected to be an RSA key in PEM format.
78 |
79 | Returns:
80 | Verifier instance.
81 |
82 | Raises:
83 | OpenSSL.crypto.Error if the key_pem can't be parsed.
84 | """
85 | if is_x509_cert:
86 | pubkey = crypto.load_certificate(crypto.FILETYPE_PEM, key_pem)
87 | else:
88 | pubkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key_pem)
89 | return OpenSSLVerifier(pubkey)
90 |
91 |
92 | class OpenSSLSigner(object):
93 | """Signs messages with a private key."""
94 |
95 | def __init__(self, pkey):
96 | """Constructor.
97 |
98 | Args:
99 | pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
100 | """
101 | self._key = pkey
102 |
103 | def sign(self, message):
104 | """Signs a message.
105 |
106 | Args:
107 | message: string, Message to be signed.
108 |
109 | Returns:
110 | string, The signature of the message for the given key.
111 | """
112 | return crypto.sign(self._key, message, 'sha256')
113 |
114 | @staticmethod
115 | def from_string(key, password='notasecret'):
116 | """Construct a Signer instance from a string.
117 |
118 | Args:
119 | key: string, private key in PKCS12 or PEM format.
120 | password: string, password for the private key file.
121 |
122 | Returns:
123 | Signer instance.
124 |
125 | Raises:
126 | OpenSSL.crypto.Error if the key can't be parsed.
127 | """
128 | if key.startswith('-----BEGIN '):
129 | pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, key)
130 | else:
131 | pkey = crypto.load_pkcs12(key, password).get_privatekey()
132 | return OpenSSLSigner(pkey)
133 |
134 | except ImportError:
135 | OpenSSLVerifier = None
136 | OpenSSLSigner = None
137 |
138 |
139 | try:
140 | from Crypto.PublicKey import RSA
141 | from Crypto.Hash import SHA256
142 | from Crypto.Signature import PKCS1_v1_5
143 |
144 |
145 | class PyCryptoVerifier(object):
146 | """Verifies the signature on a message."""
147 |
148 | def __init__(self, pubkey):
149 | """Constructor.
150 |
151 | Args:
152 | pubkey, OpenSSL.crypto.PKey (or equiv), The public key to verify with.
153 | """
154 | self._pubkey = pubkey
155 |
156 | def verify(self, message, signature):
157 | """Verifies a message against a signature.
158 |
159 | Args:
160 | message: string, The message to verify.
161 | signature: string, The signature on the message.
162 |
163 | Returns:
164 | True if message was signed by the private key associated with the public
165 | key that this object was constructed with.
166 | """
167 | try:
168 | return PKCS1_v1_5.new(self._pubkey).verify(
169 | SHA256.new(message), signature)
170 | except:
171 | return False
172 |
173 | @staticmethod
174 | def from_string(key_pem, is_x509_cert):
175 | """Construct a Verified instance from a string.
176 |
177 | Args:
178 | key_pem: string, public key in PEM format.
179 | is_x509_cert: bool, True if key_pem is an X509 cert, otherwise it is
180 | expected to be an RSA key in PEM format.
181 |
182 | Returns:
183 | Verifier instance.
184 |
185 | Raises:
186 | NotImplementedError if is_x509_cert is true.
187 | """
188 | if is_x509_cert:
189 | raise NotImplementedError(
190 | 'X509 certs are not supported by the PyCrypto library. '
191 | 'Try using PyOpenSSL if native code is an option.')
192 | else:
193 | pubkey = RSA.importKey(key_pem)
194 | return PyCryptoVerifier(pubkey)
195 |
196 |
197 | class PyCryptoSigner(object):
198 | """Signs messages with a private key."""
199 |
200 | def __init__(self, pkey):
201 | """Constructor.
202 |
203 | Args:
204 | pkey, OpenSSL.crypto.PKey (or equiv), The private key to sign with.
205 | """
206 | self._key = pkey
207 |
208 | def sign(self, message):
209 | """Signs a message.
210 |
211 | Args:
212 | message: string, Message to be signed.
213 |
214 | Returns:
215 | string, The signature of the message for the given key.
216 | """
217 | return PKCS1_v1_5.new(self._key).sign(SHA256.new(message))
218 |
219 | @staticmethod
220 | def from_string(key, password='notasecret'):
221 | """Construct a Signer instance from a string.
222 |
223 | Args:
224 | key: string, private key in PEM format.
225 | password: string, password for private key file. Unused for PEM files.
226 |
227 | Returns:
228 | Signer instance.
229 |
230 | Raises:
231 | NotImplementedError if they key isn't in PEM format.
232 | """
233 | if key.startswith('-----BEGIN '):
234 | pkey = RSA.importKey(key)
235 | else:
236 | raise NotImplementedError(
237 | 'PKCS12 format is not supported by the PyCrpto library. '
238 | 'Try converting to a "PEM" '
239 | '(openssl pkcs12 -in xxxxx.p12 -nodes -nocerts > privatekey.pem) '
240 | 'or using PyOpenSSL if native code is an option.')
241 | return PyCryptoSigner(pkey)
242 |
243 | except ImportError:
244 | PyCryptoVerifier = None
245 | PyCryptoSigner = None
246 |
247 |
248 | if OpenSSLSigner:
249 | Signer = OpenSSLSigner
250 | Verifier = OpenSSLVerifier
251 | elif PyCryptoSigner:
252 | Signer = PyCryptoSigner
253 | Verifier = PyCryptoVerifier
254 | else:
255 | raise ImportError('No encryption library found. Please install either '
256 | 'PyOpenSSL, or PyCrypto 2.6 or later')
257 |
258 |
259 | def _urlsafe_b64encode(raw_bytes):
260 | return base64.urlsafe_b64encode(raw_bytes).rstrip('=')
261 |
262 |
263 | def _urlsafe_b64decode(b64string):
264 | # Guard against unicode strings, which base64 can't handle.
265 | b64string = b64string.encode('ascii')
266 | padded = b64string + '=' * (4 - len(b64string) % 4)
267 | return base64.urlsafe_b64decode(padded)
268 |
269 |
270 | def _json_encode(data):
271 | return simplejson.dumps(data, separators = (',', ':'))
272 |
273 |
274 | def make_signed_jwt(signer, payload):
275 | """Make a signed JWT.
276 |
277 | See http://self-issued.info/docs/draft-jones-json-web-token.html.
278 |
279 | Args:
280 | signer: crypt.Signer, Cryptographic signer.
281 | payload: dict, Dictionary of data to convert to JSON and then sign.
282 |
283 | Returns:
284 | string, The JWT for the payload.
285 | """
286 | header = {'typ': 'JWT', 'alg': 'RS256'}
287 |
288 | segments = [
289 | _urlsafe_b64encode(_json_encode(header)),
290 | _urlsafe_b64encode(_json_encode(payload)),
291 | ]
292 | signing_input = '.'.join(segments)
293 |
294 | signature = signer.sign(signing_input)
295 | segments.append(_urlsafe_b64encode(signature))
296 |
297 | logger.debug(str(segments))
298 |
299 | return '.'.join(segments)
300 |
301 |
302 | def verify_signed_jwt_with_certs(jwt, certs, audience):
303 | """Verify a JWT against public certs.
304 |
305 | See http://self-issued.info/docs/draft-jones-json-web-token.html.
306 |
307 | Args:
308 | jwt: string, A JWT.
309 | certs: dict, Dictionary where values of public keys in PEM format.
310 | audience: string, The audience, 'aud', that this JWT should contain. If
311 | None then the JWT's 'aud' parameter is not verified.
312 |
313 | Returns:
314 | dict, The deserialized JSON payload in the JWT.
315 |
316 | Raises:
317 | AppIdentityError if any checks are failed.
318 | """
319 | segments = jwt.split('.')
320 |
321 | if (len(segments) != 3):
322 | raise AppIdentityError(
323 | 'Wrong number of segments in token: %s' % jwt)
324 | signed = '%s.%s' % (segments[0], segments[1])
325 |
326 | signature = _urlsafe_b64decode(segments[2])
327 |
328 | # Parse token.
329 | json_body = _urlsafe_b64decode(segments[1])
330 | try:
331 | parsed = simplejson.loads(json_body)
332 | except:
333 | raise AppIdentityError('Can\'t parse token: %s' % json_body)
334 |
335 | # Check signature.
336 | verified = False
337 | for (keyname, pem) in certs.items():
338 | verifier = Verifier.from_string(pem, True)
339 | if (verifier.verify(signed, signature)):
340 | verified = True
341 | break
342 | if not verified:
343 | raise AppIdentityError('Invalid token signature: %s' % jwt)
344 |
345 | # Check creation timestamp.
346 | iat = parsed.get('iat')
347 | if iat is None:
348 | raise AppIdentityError('No iat field in token: %s' % json_body)
349 | earliest = iat - CLOCK_SKEW_SECS
350 |
351 | # Check expiration timestamp.
352 | now = long(time.time())
353 | exp = parsed.get('exp')
354 | if exp is None:
355 | raise AppIdentityError('No exp field in token: %s' % json_body)
356 | if exp >= now + MAX_TOKEN_LIFETIME_SECS:
357 | raise AppIdentityError(
358 | 'exp field too far in future: %s' % json_body)
359 | latest = exp + CLOCK_SKEW_SECS
360 |
361 | if now < earliest:
362 | raise AppIdentityError('Token used too early, %d < %d: %s' %
363 | (now, earliest, json_body))
364 | if now > latest:
365 | raise AppIdentityError('Token used too late, %d > %d: %s' %
366 | (now, latest, json_body))
367 |
368 | # Check audience.
369 | if audience is not None:
370 | aud = parsed.get('aud')
371 | if aud is None:
372 | raise AppIdentityError('No aud field in token: %s' % json_body)
373 | if aud != audience:
374 | raise AppIdentityError('Wrong recipient, %s != %s: %s' %
375 | (aud, audience, json_body))
376 |
377 | return parsed
378 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # Copyright 2007 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 | #
17 | import cgi
18 | import datetime
19 | import time
20 | import webapp2
21 |
22 | import hashlib
23 | import pickle
24 |
25 | from google.appengine.ext import db
26 | from google.appengine.api import memcache
27 |
28 | import youtube_integration
29 | from database_tables import *
30 |
31 | GREY_CHANNELS = [ 'CGPGrey']
32 | BRADY_CHANNELS = [ 'numberphile', 'Computerphile',
33 | 'sixtysymbols', 'periodicvideos', 'nottinghamscience',
34 | 'DeepSkyVideos', 'bibledex', 'wordsoftheworld',
35 | 'FavScientist', 'psyfile', 'BackstageScience',
36 | 'foodskey', 'BradyStuff']
37 |
38 | def disp_viewcount(db_views):
39 | if db_views == 301:
40 | return '301'
41 | elif db_views >= 0:
42 | return db_views
43 | elif db_views == -1:
44 | return "<live video>"
45 | elif db_views == -2:
46 | return "<not yet calculated>"
47 | else:
48 | return "<error>"
49 |
50 | def load_front_data():
51 | bradyVids = memcache.get('bradyVids')
52 | greyVid = memcache.get('greyVid')
53 | lastUpdate = memcache.get('lastUpdate')
54 |
55 | greyViews = memcache.get('greyViews')
56 | bradyTotal = memcache.get('bradyTotal')
57 | bradyAverage = memcache.get('bradyAverage')
58 |
59 | if not (bradyVids and greyVid and lastUpdate and greyViews and bradyTotal and bradyAverage):
60 | bradyVids = db.GqlQuery("SELECT * FROM BradyVideo ORDER BY published DESC")
61 | bradyVids = list(bradyVids)
62 | memcache.set('bradyVids', bradyVids)
63 |
64 | greyVid = list(db.GqlQuery("SELECT * FROM GreyVideo ORDER BY published DESC LIMIT 1"))[0]
65 | memcache.set('greyVid', greyVid)
66 |
67 | lastUpdate = list(db.GqlQuery("SELECT * FROM UpdateLog ORDER BY update_time DESC LIMIT 1"))[0].update_time
68 | memcache.set('lastUpdate', lastUpdate)
69 |
70 | greyViews = disp_viewcount(greyVid.viewcount)
71 | memcache.set('greyViews', greyViews)
72 |
73 | countable_brady_counts = [vid.viewcount for vid in bradyVids if vid.viewcount>=0]
74 |
75 | bradyTotal = sum(countable_brady_counts)
76 | memcache.set('bradyTotal', bradyTotal)
77 |
78 |
79 | if len(countable_brady_counts)==0:
80 | bradyAvg = 'N/A'
81 | else:
82 | bradyAvg = bradyTotal/len(countable_brady_counts)
83 |
84 | memcache.set('bradyAvg', bradyAvg)
85 |
86 | return (bradyVids, greyVid, lastUpdate, greyViews, bradyTotal, bradyAvg)
87 |
88 | def esc(s):
89 | return cgi.escape(s,quote=True)
90 |
91 |
92 | class Handler(webapp2.RequestHandler):
93 | def write(self,content):
94 | self.response.out.write(content)
95 |
96 | def set_cookie(self, name, value, expires=None, path='/', domain=None):
97 | extra_info = ''
98 | if expires:
99 | extra_info += 'expires=%s; '%expires
100 | if path:
101 | extra_info += 'path=%s; '%path
102 | if domain:
103 | extra_info += 'domain=%s; '%domain
104 | if extra_info:
105 | extra_info = '; ' + extra_info[:-2]
106 | main_statement = '%s=%s'%(name,value)
107 | set_cookie_val = main_statement + extra_info
108 | self.response.headers.add_header('Set-Cookie',set_cookie_val)
109 |
110 | def clear_cookie(self, name):
111 | cookie = self.read_cookie(name)
112 | if not cookie:
113 | return
114 | self.set_cookie(name, '', path=None)
115 |
116 | def read_cookie(self, name, alt=None):
117 | return self.request.cookies.get(name,alt)
118 |
119 | page_template = """
120 |
121 |
122 |
123 |
124 | Brady vs. Grey | Original
125 |
136 |
137 |
146 |
147 |
148 | Q: How many videos has Brady Haran released since C.G.P. Grey last released a video?
149 | A:
150 |
151 | %(number)s
152 |
153 |
154 |
155 |
156 | Creator
157 |
158 |
159 | Channel
160 |
161 |
162 | Uploaded
163 |
164 |
165 | View Count
166 |
167 |
168 | Title/Link
169 |
170 |
171 | %(rows)s
172 |
173 |
174 | Q: How do their view counts compare? A:
175 |
176 |
177 |
178 |
Grey
179 |
%(grey_views)s
180 |
181 |
182 |
Brady: Average
183 |
%(brady_avg)s
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
Brady: Total
192 |
%(brady_total)s
193 |
194 |
195 |
196 |
197 | Last updated: %(refresh_date)s.
198 | Powered by YouTube Data API (v3).
199 |
200 | View the source code on GitHub.
201 |
202 | Notes:
203 |
204 |
205 | To conserve processing power and network resources, this app should update only four times per day: 00:00, 06:00, 12:00, and 18:00 UTC.
206 |
207 |
208 | A new version of this web app is now available here.
209 |
210 |
211 | Recently, a pull request by Github user Eseb ensured that Grey's 2nd channel is not counted. This is a response to the video "⌘X 22.1: Seventeen".
212 |
389 |
390 |
391 | """
392 | def handle_500(request, response, exception):
393 | response.clear()
394 | response.write(e500)
395 | app.error_handlers[500] = handle_500
396 |
--------------------------------------------------------------------------------
/oauth2client/locked_file.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Locked file interface that should work on Unix and Windows pythons.
16 |
17 | This module first tries to use fcntl locking to ensure serialized access
18 | to a file, then falls back on a lock file if that is unavialable.
19 |
20 | Usage:
21 | f = LockedFile('filename', 'r+b', 'rb')
22 | f.open_and_lock()
23 | if f.is_locked():
24 | print 'Acquired filename with r+b mode'
25 | f.file_handle().write('locked data')
26 | else:
27 | print 'Aquired filename with rb mode'
28 | f.unlock_and_close()
29 | """
30 |
31 | __author__ = 'cache@google.com (David T McWherter)'
32 |
33 | import errno
34 | import logging
35 | import os
36 | import time
37 |
38 | from oauth2client import util
39 |
40 | logger = logging.getLogger(__name__)
41 |
42 |
43 | class CredentialsFileSymbolicLinkError(Exception):
44 | """Credentials files must not be symbolic links."""
45 |
46 |
47 | class AlreadyLockedException(Exception):
48 | """Trying to lock a file that has already been locked by the LockedFile."""
49 | pass
50 |
51 |
52 | def validate_file(filename):
53 | if os.path.islink(filename):
54 | raise CredentialsFileSymbolicLinkError(
55 | 'File: %s is a symbolic link.' % filename)
56 |
57 | class _Opener(object):
58 | """Base class for different locking primitives."""
59 |
60 | def __init__(self, filename, mode, fallback_mode):
61 | """Create an Opener.
62 |
63 | Args:
64 | filename: string, The pathname of the file.
65 | mode: string, The preferred mode to access the file with.
66 | fallback_mode: string, The mode to use if locking fails.
67 | """
68 | self._locked = False
69 | self._filename = filename
70 | self._mode = mode
71 | self._fallback_mode = fallback_mode
72 | self._fh = None
73 |
74 | def is_locked(self):
75 | """Was the file locked."""
76 | return self._locked
77 |
78 | def file_handle(self):
79 | """The file handle to the file. Valid only after opened."""
80 | return self._fh
81 |
82 | def filename(self):
83 | """The filename that is being locked."""
84 | return self._filename
85 |
86 | def open_and_lock(self, timeout, delay):
87 | """Open the file and lock it.
88 |
89 | Args:
90 | timeout: float, How long to try to lock for.
91 | delay: float, How long to wait between retries.
92 | """
93 | pass
94 |
95 | def unlock_and_close(self):
96 | """Unlock and close the file."""
97 | pass
98 |
99 |
100 | class _PosixOpener(_Opener):
101 | """Lock files using Posix advisory lock files."""
102 |
103 | def open_and_lock(self, timeout, delay):
104 | """Open the file and lock it.
105 |
106 | Tries to create a .lock file next to the file we're trying to open.
107 |
108 | Args:
109 | timeout: float, How long to try to lock for.
110 | delay: float, How long to wait between retries.
111 |
112 | Raises:
113 | AlreadyLockedException: if the lock is already acquired.
114 | IOError: if the open fails.
115 | CredentialsFileSymbolicLinkError if the file is a symbolic link.
116 | """
117 | if self._locked:
118 | raise AlreadyLockedException('File %s is already locked' %
119 | self._filename)
120 | self._locked = False
121 |
122 | validate_file(self._filename)
123 | try:
124 | self._fh = open(self._filename, self._mode)
125 | except IOError, e:
126 | # If we can't access with _mode, try _fallback_mode and don't lock.
127 | if e.errno == errno.EACCES:
128 | self._fh = open(self._filename, self._fallback_mode)
129 | return
130 |
131 | lock_filename = self._posix_lockfile(self._filename)
132 | start_time = time.time()
133 | while True:
134 | try:
135 | self._lock_fd = os.open(lock_filename,
136 | os.O_CREAT|os.O_EXCL|os.O_RDWR)
137 | self._locked = True
138 | break
139 |
140 | except OSError, e:
141 | if e.errno != errno.EEXIST:
142 | raise
143 | if (time.time() - start_time) >= timeout:
144 | logger.warn('Could not acquire lock %s in %s seconds' % (
145 | lock_filename, timeout))
146 | # Close the file and open in fallback_mode.
147 | if self._fh:
148 | self._fh.close()
149 | self._fh = open(self._filename, self._fallback_mode)
150 | return
151 | time.sleep(delay)
152 |
153 | def unlock_and_close(self):
154 | """Unlock a file by removing the .lock file, and close the handle."""
155 | if self._locked:
156 | lock_filename = self._posix_lockfile(self._filename)
157 | os.close(self._lock_fd)
158 | os.unlink(lock_filename)
159 | self._locked = False
160 | self._lock_fd = None
161 | if self._fh:
162 | self._fh.close()
163 |
164 | def _posix_lockfile(self, filename):
165 | """The name of the lock file to use for posix locking."""
166 | return '%s.lock' % filename
167 |
168 |
169 | try:
170 | import fcntl
171 |
172 | class _FcntlOpener(_Opener):
173 | """Open, lock, and unlock a file using fcntl.lockf."""
174 |
175 | def open_and_lock(self, timeout, delay):
176 | """Open the file and lock it.
177 |
178 | Args:
179 | timeout: float, How long to try to lock for.
180 | delay: float, How long to wait between retries
181 |
182 | Raises:
183 | AlreadyLockedException: if the lock is already acquired.
184 | IOError: if the open fails.
185 | CredentialsFileSymbolicLinkError if the file is a symbolic link.
186 | """
187 | if self._locked:
188 | raise AlreadyLockedException('File %s is already locked' %
189 | self._filename)
190 | start_time = time.time()
191 |
192 | validate_file(self._filename)
193 | try:
194 | self._fh = open(self._filename, self._mode)
195 | except IOError, e:
196 | # If we can't access with _mode, try _fallback_mode and don't lock.
197 | if e.errno == errno.EACCES:
198 | self._fh = open(self._filename, self._fallback_mode)
199 | return
200 |
201 | # We opened in _mode, try to lock the file.
202 | while True:
203 | try:
204 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_EX)
205 | self._locked = True
206 | return
207 | except IOError, e:
208 | # If not retrying, then just pass on the error.
209 | if timeout == 0:
210 | raise e
211 | if e.errno != errno.EACCES:
212 | raise e
213 | # We could not acquire the lock. Try again.
214 | if (time.time() - start_time) >= timeout:
215 | logger.warn('Could not lock %s in %s seconds' % (
216 | self._filename, timeout))
217 | if self._fh:
218 | self._fh.close()
219 | self._fh = open(self._filename, self._fallback_mode)
220 | return
221 | time.sleep(delay)
222 |
223 | def unlock_and_close(self):
224 | """Close and unlock the file using the fcntl.lockf primitive."""
225 | if self._locked:
226 | fcntl.lockf(self._fh.fileno(), fcntl.LOCK_UN)
227 | self._locked = False
228 | if self._fh:
229 | self._fh.close()
230 | except ImportError:
231 | _FcntlOpener = None
232 |
233 |
234 | try:
235 | import pywintypes
236 | import win32con
237 | import win32file
238 |
239 | class _Win32Opener(_Opener):
240 | """Open, lock, and unlock a file using windows primitives."""
241 |
242 | # Error #33:
243 | # 'The process cannot access the file because another process'
244 | FILE_IN_USE_ERROR = 33
245 |
246 | # Error #158:
247 | # 'The segment is already unlocked.'
248 | FILE_ALREADY_UNLOCKED_ERROR = 158
249 |
250 | def open_and_lock(self, timeout, delay):
251 | """Open the file and lock it.
252 |
253 | Args:
254 | timeout: float, How long to try to lock for.
255 | delay: float, How long to wait between retries
256 |
257 | Raises:
258 | AlreadyLockedException: if the lock is already acquired.
259 | IOError: if the open fails.
260 | CredentialsFileSymbolicLinkError if the file is a symbolic link.
261 | """
262 | if self._locked:
263 | raise AlreadyLockedException('File %s is already locked' %
264 | self._filename)
265 | start_time = time.time()
266 |
267 | validate_file(self._filename)
268 | try:
269 | self._fh = open(self._filename, self._mode)
270 | except IOError, e:
271 | # If we can't access with _mode, try _fallback_mode and don't lock.
272 | if e.errno == errno.EACCES:
273 | self._fh = open(self._filename, self._fallback_mode)
274 | return
275 |
276 | # We opened in _mode, try to lock the file.
277 | while True:
278 | try:
279 | hfile = win32file._get_osfhandle(self._fh.fileno())
280 | win32file.LockFileEx(
281 | hfile,
282 | (win32con.LOCKFILE_FAIL_IMMEDIATELY|
283 | win32con.LOCKFILE_EXCLUSIVE_LOCK), 0, -0x10000,
284 | pywintypes.OVERLAPPED())
285 | self._locked = True
286 | return
287 | except pywintypes.error, e:
288 | if timeout == 0:
289 | raise e
290 |
291 | # If the error is not that the file is already in use, raise.
292 | if e[0] != _Win32Opener.FILE_IN_USE_ERROR:
293 | raise
294 |
295 | # We could not acquire the lock. Try again.
296 | if (time.time() - start_time) >= timeout:
297 | logger.warn('Could not lock %s in %s seconds' % (
298 | self._filename, timeout))
299 | if self._fh:
300 | self._fh.close()
301 | self._fh = open(self._filename, self._fallback_mode)
302 | return
303 | time.sleep(delay)
304 |
305 | def unlock_and_close(self):
306 | """Close and unlock the file using the win32 primitive."""
307 | if self._locked:
308 | try:
309 | hfile = win32file._get_osfhandle(self._fh.fileno())
310 | win32file.UnlockFileEx(hfile, 0, -0x10000, pywintypes.OVERLAPPED())
311 | except pywintypes.error, e:
312 | if e[0] != _Win32Opener.FILE_ALREADY_UNLOCKED_ERROR:
313 | raise
314 | self._locked = False
315 | if self._fh:
316 | self._fh.close()
317 | except ImportError:
318 | _Win32Opener = None
319 |
320 |
321 | class LockedFile(object):
322 | """Represent a file that has exclusive access."""
323 |
324 | @util.positional(4)
325 | def __init__(self, filename, mode, fallback_mode, use_native_locking=True):
326 | """Construct a LockedFile.
327 |
328 | Args:
329 | filename: string, The path of the file to open.
330 | mode: string, The mode to try to open the file with.
331 | fallback_mode: string, The mode to use if locking fails.
332 | use_native_locking: bool, Whether or not fcntl/win32 locking is used.
333 | """
334 | opener = None
335 | if not opener and use_native_locking:
336 | if _Win32Opener:
337 | opener = _Win32Opener(filename, mode, fallback_mode)
338 | if _FcntlOpener:
339 | opener = _FcntlOpener(filename, mode, fallback_mode)
340 |
341 | if not opener:
342 | opener = _PosixOpener(filename, mode, fallback_mode)
343 |
344 | self._opener = opener
345 |
346 | def filename(self):
347 | """Return the filename we were constructed with."""
348 | return self._opener._filename
349 |
350 | def file_handle(self):
351 | """Return the file_handle to the opened file."""
352 | return self._opener.file_handle()
353 |
354 | def is_locked(self):
355 | """Return whether we successfully locked the file."""
356 | return self._opener.is_locked()
357 |
358 | def open_and_lock(self, timeout=0, delay=0.05):
359 | """Open the file, trying to lock it.
360 |
361 | Args:
362 | timeout: float, The number of seconds to try to acquire the lock.
363 | delay: float, The number of seconds to wait between retry attempts.
364 |
365 | Raises:
366 | AlreadyLockedException: if the lock is already acquired.
367 | IOError: if the open fails.
368 | """
369 | self._opener.open_and_lock(timeout, delay)
370 |
371 | def unlock_and_close(self):
372 | """Unlock and close a file."""
373 | self._opener.unlock_and_close()
374 |
--------------------------------------------------------------------------------
/apiclient/model.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python2.4
2 | #
3 | # Copyright (C) 2010 Google Inc.
4 | #
5 | # Licensed under the Apache License, Version 2.0 (the "License");
6 | # you may not use this file except in compliance with the License.
7 | # You may obtain a copy of the License at
8 | #
9 | # http://www.apache.org/licenses/LICENSE-2.0
10 | #
11 | # Unless required by applicable law or agreed to in writing, software
12 | # distributed under the License is distributed on an "AS IS" BASIS,
13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 | # See the License for the specific language governing permissions and
15 | # limitations under the License.
16 |
17 | """Model objects for requests and responses.
18 |
19 | Each API may support one or more serializations, such
20 | as JSON, Atom, etc. The model classes are responsible
21 | for converting between the wire format and the Python
22 | object representation.
23 | """
24 |
25 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
26 |
27 | import logging
28 | import urllib
29 |
30 | from apiclient import __version__
31 | from errors import HttpError
32 | from oauth2client.anyjson import simplejson
33 |
34 |
35 | dump_request_response = False
36 |
37 |
38 | def _abstract():
39 | raise NotImplementedError('You need to override this function')
40 |
41 |
42 | class Model(object):
43 | """Model base class.
44 |
45 | All Model classes should implement this interface.
46 | The Model serializes and de-serializes between a wire
47 | format such as JSON and a Python object representation.
48 | """
49 |
50 | def request(self, headers, path_params, query_params, body_value):
51 | """Updates outgoing requests with a serialized body.
52 |
53 | Args:
54 | headers: dict, request headers
55 | path_params: dict, parameters that appear in the request path
56 | query_params: dict, parameters that appear in the query
57 | body_value: object, the request body as a Python object, which must be
58 | serializable.
59 | Returns:
60 | A tuple of (headers, path_params, query, body)
61 |
62 | headers: dict, request headers
63 | path_params: dict, parameters that appear in the request path
64 | query: string, query part of the request URI
65 | body: string, the body serialized in the desired wire format.
66 | """
67 | _abstract()
68 |
69 | def response(self, resp, content):
70 | """Convert the response wire format into a Python object.
71 |
72 | Args:
73 | resp: httplib2.Response, the HTTP response headers and status
74 | content: string, the body of the HTTP response
75 |
76 | Returns:
77 | The body de-serialized as a Python object.
78 |
79 | Raises:
80 | apiclient.errors.HttpError if a non 2xx response is received.
81 | """
82 | _abstract()
83 |
84 |
85 | class BaseModel(Model):
86 | """Base model class.
87 |
88 | Subclasses should provide implementations for the "serialize" and
89 | "deserialize" methods, as well as values for the following class attributes.
90 |
91 | Attributes:
92 | accept: The value to use for the HTTP Accept header.
93 | content_type: The value to use for the HTTP Content-type header.
94 | no_content_response: The value to return when deserializing a 204 "No
95 | Content" response.
96 | alt_param: The value to supply as the "alt" query parameter for requests.
97 | """
98 |
99 | accept = None
100 | content_type = None
101 | no_content_response = None
102 | alt_param = None
103 |
104 | def _log_request(self, headers, path_params, query, body):
105 | """Logs debugging information about the request if requested."""
106 | if dump_request_response:
107 | logging.info('--request-start--')
108 | logging.info('-headers-start-')
109 | for h, v in headers.iteritems():
110 | logging.info('%s: %s', h, v)
111 | logging.info('-headers-end-')
112 | logging.info('-path-parameters-start-')
113 | for h, v in path_params.iteritems():
114 | logging.info('%s: %s', h, v)
115 | logging.info('-path-parameters-end-')
116 | logging.info('body: %s', body)
117 | logging.info('query: %s', query)
118 | logging.info('--request-end--')
119 |
120 | def request(self, headers, path_params, query_params, body_value):
121 | """Updates outgoing requests with a serialized body.
122 |
123 | Args:
124 | headers: dict, request headers
125 | path_params: dict, parameters that appear in the request path
126 | query_params: dict, parameters that appear in the query
127 | body_value: object, the request body as a Python object, which must be
128 | serializable by simplejson.
129 | Returns:
130 | A tuple of (headers, path_params, query, body)
131 |
132 | headers: dict, request headers
133 | path_params: dict, parameters that appear in the request path
134 | query: string, query part of the request URI
135 | body: string, the body serialized as JSON
136 | """
137 | query = self._build_query(query_params)
138 | headers['accept'] = self.accept
139 | headers['accept-encoding'] = 'gzip, deflate'
140 | if 'user-agent' in headers:
141 | headers['user-agent'] += ' '
142 | else:
143 | headers['user-agent'] = ''
144 | headers['user-agent'] += 'google-api-python-client/%s (gzip)' % __version__
145 |
146 | if body_value is not None:
147 | headers['content-type'] = self.content_type
148 | body_value = self.serialize(body_value)
149 | self._log_request(headers, path_params, query, body_value)
150 | return (headers, path_params, query, body_value)
151 |
152 | def _build_query(self, params):
153 | """Builds a query string.
154 |
155 | Args:
156 | params: dict, the query parameters
157 |
158 | Returns:
159 | The query parameters properly encoded into an HTTP URI query string.
160 | """
161 | if self.alt_param is not None:
162 | params.update({'alt': self.alt_param})
163 | astuples = []
164 | for key, value in params.iteritems():
165 | if type(value) == type([]):
166 | for x in value:
167 | x = x.encode('utf-8')
168 | astuples.append((key, x))
169 | else:
170 | if getattr(value, 'encode', False) and callable(value.encode):
171 | value = value.encode('utf-8')
172 | astuples.append((key, value))
173 | return '?' + urllib.urlencode(astuples)
174 |
175 | def _log_response(self, resp, content):
176 | """Logs debugging information about the response if requested."""
177 | if dump_request_response:
178 | logging.info('--response-start--')
179 | for h, v in resp.iteritems():
180 | logging.info('%s: %s', h, v)
181 | if content:
182 | logging.info(content)
183 | logging.info('--response-end--')
184 |
185 | def response(self, resp, content):
186 | """Convert the response wire format into a Python object.
187 |
188 | Args:
189 | resp: httplib2.Response, the HTTP response headers and status
190 | content: string, the body of the HTTP response
191 |
192 | Returns:
193 | The body de-serialized as a Python object.
194 |
195 | Raises:
196 | apiclient.errors.HttpError if a non 2xx response is received.
197 | """
198 | self._log_response(resp, content)
199 | # Error handling is TBD, for example, do we retry
200 | # for some operation/error combinations?
201 | if resp.status < 300:
202 | if resp.status == 204:
203 | # A 204: No Content response should be treated differently
204 | # to all the other success states
205 | return self.no_content_response
206 | return self.deserialize(content)
207 | else:
208 | logging.debug('Content from bad request was: %s' % content)
209 | raise HttpError(resp, content)
210 |
211 | def serialize(self, body_value):
212 | """Perform the actual Python object serialization.
213 |
214 | Args:
215 | body_value: object, the request body as a Python object.
216 |
217 | Returns:
218 | string, the body in serialized form.
219 | """
220 | _abstract()
221 |
222 | def deserialize(self, content):
223 | """Perform the actual deserialization from response string to Python
224 | object.
225 |
226 | Args:
227 | content: string, the body of the HTTP response
228 |
229 | Returns:
230 | The body de-serialized as a Python object.
231 | """
232 | _abstract()
233 |
234 |
235 | class JsonModel(BaseModel):
236 | """Model class for JSON.
237 |
238 | Serializes and de-serializes between JSON and the Python
239 | object representation of HTTP request and response bodies.
240 | """
241 | accept = 'application/json'
242 | content_type = 'application/json'
243 | alt_param = 'json'
244 |
245 | def __init__(self, data_wrapper=False):
246 | """Construct a JsonModel.
247 |
248 | Args:
249 | data_wrapper: boolean, wrap requests and responses in a data wrapper
250 | """
251 | self._data_wrapper = data_wrapper
252 |
253 | def serialize(self, body_value):
254 | if (isinstance(body_value, dict) and 'data' not in body_value and
255 | self._data_wrapper):
256 | body_value = {'data': body_value}
257 | return simplejson.dumps(body_value)
258 |
259 | def deserialize(self, content):
260 | content = content.decode('utf-8')
261 | body = simplejson.loads(content)
262 | if self._data_wrapper and isinstance(body, dict) and 'data' in body:
263 | body = body['data']
264 | return body
265 |
266 | @property
267 | def no_content_response(self):
268 | return {}
269 |
270 |
271 | class RawModel(JsonModel):
272 | """Model class for requests that don't return JSON.
273 |
274 | Serializes and de-serializes between JSON and the Python
275 | object representation of HTTP request, and returns the raw bytes
276 | of the response body.
277 | """
278 | accept = '*/*'
279 | content_type = 'application/json'
280 | alt_param = None
281 |
282 | def deserialize(self, content):
283 | return content
284 |
285 | @property
286 | def no_content_response(self):
287 | return ''
288 |
289 |
290 | class MediaModel(JsonModel):
291 | """Model class for requests that return Media.
292 |
293 | Serializes and de-serializes between JSON and the Python
294 | object representation of HTTP request, and returns the raw bytes
295 | of the response body.
296 | """
297 | accept = '*/*'
298 | content_type = 'application/json'
299 | alt_param = 'media'
300 |
301 | def deserialize(self, content):
302 | return content
303 |
304 | @property
305 | def no_content_response(self):
306 | return ''
307 |
308 |
309 | class ProtocolBufferModel(BaseModel):
310 | """Model class for protocol buffers.
311 |
312 | Serializes and de-serializes the binary protocol buffer sent in the HTTP
313 | request and response bodies.
314 | """
315 | accept = 'application/x-protobuf'
316 | content_type = 'application/x-protobuf'
317 | alt_param = 'proto'
318 |
319 | def __init__(self, protocol_buffer):
320 | """Constructs a ProtocolBufferModel.
321 |
322 | The serialzed protocol buffer returned in an HTTP response will be
323 | de-serialized using the given protocol buffer class.
324 |
325 | Args:
326 | protocol_buffer: The protocol buffer class used to de-serialize a
327 | response from the API.
328 | """
329 | self._protocol_buffer = protocol_buffer
330 |
331 | def serialize(self, body_value):
332 | return body_value.SerializeToString()
333 |
334 | def deserialize(self, content):
335 | return self._protocol_buffer.FromString(content)
336 |
337 | @property
338 | def no_content_response(self):
339 | return self._protocol_buffer()
340 |
341 |
342 | def makepatch(original, modified):
343 | """Create a patch object.
344 |
345 | Some methods support PATCH, an efficient way to send updates to a resource.
346 | This method allows the easy construction of patch bodies by looking at the
347 | differences between a resource before and after it was modified.
348 |
349 | Args:
350 | original: object, the original deserialized resource
351 | modified: object, the modified deserialized resource
352 | Returns:
353 | An object that contains only the changes from original to modified, in a
354 | form suitable to pass to a PATCH method.
355 |
356 | Example usage:
357 | item = service.activities().get(postid=postid, userid=userid).execute()
358 | original = copy.deepcopy(item)
359 | item['object']['content'] = 'This is updated.'
360 | service.activities.patch(postid=postid, userid=userid,
361 | body=makepatch(original, item)).execute()
362 | """
363 | patch = {}
364 | for key, original_value in original.iteritems():
365 | modified_value = modified.get(key, None)
366 | if modified_value is None:
367 | # Use None to signal that the element is deleted
368 | patch[key] = None
369 | elif original_value != modified_value:
370 | if type(original_value) == type({}):
371 | # Recursively descend objects
372 | patch[key] = makepatch(original_value, modified_value)
373 | else:
374 | # In the case of simple types or arrays we just replace
375 | patch[key] = modified_value
376 | else:
377 | # Don't add anything to patch if there's no change
378 | pass
379 | for key in modified:
380 | if key not in original:
381 | patch[key] = modified[key]
382 |
383 | return patch
384 |
--------------------------------------------------------------------------------
/oauth2client/multistore_file.py:
--------------------------------------------------------------------------------
1 | # Copyright 2011 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Multi-credential file store with lock support.
16 |
17 | This module implements a JSON credential store where multiple
18 | credentials can be stored in one file. That file supports locking
19 | both in a single process and across processes.
20 |
21 | The credential themselves are keyed off of:
22 | * client_id
23 | * user_agent
24 | * scope
25 |
26 | The format of the stored data is like so:
27 | {
28 | 'file_version': 1,
29 | 'data': [
30 | {
31 | 'key': {
32 | 'clientId': '',
33 | 'userAgent': '',
34 | 'scope': ''
35 | },
36 | 'credential': {
37 | # JSON serialized Credentials.
38 | }
39 | }
40 | ]
41 | }
42 | """
43 |
44 | __author__ = 'jbeda@google.com (Joe Beda)'
45 |
46 | import base64
47 | import errno
48 | import logging
49 | import os
50 | import threading
51 |
52 | from anyjson import simplejson
53 | from oauth2client.client import Storage as BaseStorage
54 | from oauth2client.client import Credentials
55 | from oauth2client import util
56 | from locked_file import LockedFile
57 |
58 | logger = logging.getLogger(__name__)
59 |
60 | # A dict from 'filename'->_MultiStore instances
61 | _multistores = {}
62 | _multistores_lock = threading.Lock()
63 |
64 |
65 | class Error(Exception):
66 | """Base error for this module."""
67 | pass
68 |
69 |
70 | class NewerCredentialStoreError(Error):
71 | """The credential store is a newer version that supported."""
72 | pass
73 |
74 |
75 | @util.positional(4)
76 | def get_credential_storage(filename, client_id, user_agent, scope,
77 | warn_on_readonly=True):
78 | """Get a Storage instance for a credential.
79 |
80 | Args:
81 | filename: The JSON file storing a set of credentials
82 | client_id: The client_id for the credential
83 | user_agent: The user agent for the credential
84 | scope: string or iterable of strings, Scope(s) being requested
85 | warn_on_readonly: if True, log a warning if the store is readonly
86 |
87 | Returns:
88 | An object derived from client.Storage for getting/setting the
89 | credential.
90 | """
91 | # Recreate the legacy key with these specific parameters
92 | key = {'clientId': client_id, 'userAgent': user_agent,
93 | 'scope': util.scopes_to_string(scope)}
94 | return get_credential_storage_custom_key(
95 | filename, key, warn_on_readonly=warn_on_readonly)
96 |
97 |
98 | @util.positional(2)
99 | def get_credential_storage_custom_string_key(
100 | filename, key_string, warn_on_readonly=True):
101 | """Get a Storage instance for a credential using a single string as a key.
102 |
103 | Allows you to provide a string as a custom key that will be used for
104 | credential storage and retrieval.
105 |
106 | Args:
107 | filename: The JSON file storing a set of credentials
108 | key_string: A string to use as the key for storing this credential.
109 | warn_on_readonly: if True, log a warning if the store is readonly
110 |
111 | Returns:
112 | An object derived from client.Storage for getting/setting the
113 | credential.
114 | """
115 | # Create a key dictionary that can be used
116 | key_dict = {'key': key_string}
117 | return get_credential_storage_custom_key(
118 | filename, key_dict, warn_on_readonly=warn_on_readonly)
119 |
120 |
121 | @util.positional(2)
122 | def get_credential_storage_custom_key(
123 | filename, key_dict, warn_on_readonly=True):
124 | """Get a Storage instance for a credential using a dictionary as a key.
125 |
126 | Allows you to provide a dictionary as a custom key that will be used for
127 | credential storage and retrieval.
128 |
129 | Args:
130 | filename: The JSON file storing a set of credentials
131 | key_dict: A dictionary to use as the key for storing this credential. There
132 | is no ordering of the keys in the dictionary. Logically equivalent
133 | dictionaries will produce equivalent storage keys.
134 | warn_on_readonly: if True, log a warning if the store is readonly
135 |
136 | Returns:
137 | An object derived from client.Storage for getting/setting the
138 | credential.
139 | """
140 | multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
141 | key = util.dict_to_tuple_key(key_dict)
142 | return multistore._get_storage(key)
143 |
144 |
145 | @util.positional(1)
146 | def get_all_credential_keys(filename, warn_on_readonly=True):
147 | """Gets all the registered credential keys in the given Multistore.
148 |
149 | Args:
150 | filename: The JSON file storing a set of credentials
151 | warn_on_readonly: if True, log a warning if the store is readonly
152 |
153 | Returns:
154 | A list of the credential keys present in the file. They are returned as
155 | dictionaries that can be passed into get_credential_storage_custom_key to
156 | get the actual credentials.
157 | """
158 | multistore = _get_multistore(filename, warn_on_readonly=warn_on_readonly)
159 | multistore._lock()
160 | try:
161 | return multistore._get_all_credential_keys()
162 | finally:
163 | multistore._unlock()
164 |
165 |
166 | @util.positional(1)
167 | def _get_multistore(filename, warn_on_readonly=True):
168 | """A helper method to initialize the multistore with proper locking.
169 |
170 | Args:
171 | filename: The JSON file storing a set of credentials
172 | warn_on_readonly: if True, log a warning if the store is readonly
173 |
174 | Returns:
175 | A multistore object
176 | """
177 | filename = os.path.expanduser(filename)
178 | _multistores_lock.acquire()
179 | try:
180 | multistore = _multistores.setdefault(
181 | filename, _MultiStore(filename, warn_on_readonly=warn_on_readonly))
182 | finally:
183 | _multistores_lock.release()
184 | return multistore
185 |
186 |
187 | class _MultiStore(object):
188 | """A file backed store for multiple credentials."""
189 |
190 | @util.positional(2)
191 | def __init__(self, filename, warn_on_readonly=True):
192 | """Initialize the class.
193 |
194 | This will create the file if necessary.
195 | """
196 | self._file = LockedFile(filename, 'r+b', 'rb')
197 | self._thread_lock = threading.Lock()
198 | self._read_only = False
199 | self._warn_on_readonly = warn_on_readonly
200 |
201 | self._create_file_if_needed()
202 |
203 | # Cache of deserialized store. This is only valid after the
204 | # _MultiStore is locked or _refresh_data_cache is called. This is
205 | # of the form of:
206 | #
207 | # ((key, value), (key, value)...) -> OAuth2Credential
208 | #
209 | # If this is None, then the store hasn't been read yet.
210 | self._data = None
211 |
212 | class _Storage(BaseStorage):
213 | """A Storage object that knows how to read/write a single credential."""
214 |
215 | def __init__(self, multistore, key):
216 | self._multistore = multistore
217 | self._key = key
218 |
219 | def acquire_lock(self):
220 | """Acquires any lock necessary to access this Storage.
221 |
222 | This lock is not reentrant.
223 | """
224 | self._multistore._lock()
225 |
226 | def release_lock(self):
227 | """Release the Storage lock.
228 |
229 | Trying to release a lock that isn't held will result in a
230 | RuntimeError.
231 | """
232 | self._multistore._unlock()
233 |
234 | def locked_get(self):
235 | """Retrieve credential.
236 |
237 | The Storage lock must be held when this is called.
238 |
239 | Returns:
240 | oauth2client.client.Credentials
241 | """
242 | credential = self._multistore._get_credential(self._key)
243 | if credential:
244 | credential.set_store(self)
245 | return credential
246 |
247 | def locked_put(self, credentials):
248 | """Write a credential.
249 |
250 | The Storage lock must be held when this is called.
251 |
252 | Args:
253 | credentials: Credentials, the credentials to store.
254 | """
255 | self._multistore._update_credential(self._key, credentials)
256 |
257 | def locked_delete(self):
258 | """Delete a credential.
259 |
260 | The Storage lock must be held when this is called.
261 |
262 | Args:
263 | credentials: Credentials, the credentials to store.
264 | """
265 | self._multistore._delete_credential(self._key)
266 |
267 | def _create_file_if_needed(self):
268 | """Create an empty file if necessary.
269 |
270 | This method will not initialize the file. Instead it implements a
271 | simple version of "touch" to ensure the file has been created.
272 | """
273 | if not os.path.exists(self._file.filename()):
274 | old_umask = os.umask(0177)
275 | try:
276 | open(self._file.filename(), 'a+b').close()
277 | finally:
278 | os.umask(old_umask)
279 |
280 | def _lock(self):
281 | """Lock the entire multistore."""
282 | self._thread_lock.acquire()
283 | self._file.open_and_lock()
284 | if not self._file.is_locked():
285 | self._read_only = True
286 | if self._warn_on_readonly:
287 | logger.warn('The credentials file (%s) is not writable. Opening in '
288 | 'read-only mode. Any refreshed credentials will only be '
289 | 'valid for this run.' % self._file.filename())
290 | if os.path.getsize(self._file.filename()) == 0:
291 | logger.debug('Initializing empty multistore file')
292 | # The multistore is empty so write out an empty file.
293 | self._data = {}
294 | self._write()
295 | elif not self._read_only or self._data is None:
296 | # Only refresh the data if we are read/write or we haven't
297 | # cached the data yet. If we are readonly, we assume is isn't
298 | # changing out from under us and that we only have to read it
299 | # once. This prevents us from whacking any new access keys that
300 | # we have cached in memory but were unable to write out.
301 | self._refresh_data_cache()
302 |
303 | def _unlock(self):
304 | """Release the lock on the multistore."""
305 | self._file.unlock_and_close()
306 | self._thread_lock.release()
307 |
308 | def _locked_json_read(self):
309 | """Get the raw content of the multistore file.
310 |
311 | The multistore must be locked when this is called.
312 |
313 | Returns:
314 | The contents of the multistore decoded as JSON.
315 | """
316 | assert self._thread_lock.locked()
317 | self._file.file_handle().seek(0)
318 | return simplejson.load(self._file.file_handle())
319 |
320 | def _locked_json_write(self, data):
321 | """Write a JSON serializable data structure to the multistore.
322 |
323 | The multistore must be locked when this is called.
324 |
325 | Args:
326 | data: The data to be serialized and written.
327 | """
328 | assert self._thread_lock.locked()
329 | if self._read_only:
330 | return
331 | self._file.file_handle().seek(0)
332 | simplejson.dump(data, self._file.file_handle(), sort_keys=True, indent=2)
333 | self._file.file_handle().truncate()
334 |
335 | def _refresh_data_cache(self):
336 | """Refresh the contents of the multistore.
337 |
338 | The multistore must be locked when this is called.
339 |
340 | Raises:
341 | NewerCredentialStoreError: Raised when a newer client has written the
342 | store.
343 | """
344 | self._data = {}
345 | try:
346 | raw_data = self._locked_json_read()
347 | except Exception:
348 | logger.warn('Credential data store could not be loaded. '
349 | 'Will ignore and overwrite.')
350 | return
351 |
352 | version = 0
353 | try:
354 | version = raw_data['file_version']
355 | except Exception:
356 | logger.warn('Missing version for credential data store. It may be '
357 | 'corrupt or an old version. Overwriting.')
358 | if version > 1:
359 | raise NewerCredentialStoreError(
360 | 'Credential file has file_version of %d. '
361 | 'Only file_version of 1 is supported.' % version)
362 |
363 | credentials = []
364 | try:
365 | credentials = raw_data['data']
366 | except (TypeError, KeyError):
367 | pass
368 |
369 | for cred_entry in credentials:
370 | try:
371 | (key, credential) = self._decode_credential_from_json(cred_entry)
372 | self._data[key] = credential
373 | except:
374 | # If something goes wrong loading a credential, just ignore it
375 | logger.info('Error decoding credential, skipping', exc_info=True)
376 |
377 | def _decode_credential_from_json(self, cred_entry):
378 | """Load a credential from our JSON serialization.
379 |
380 | Args:
381 | cred_entry: A dict entry from the data member of our format
382 |
383 | Returns:
384 | (key, cred) where the key is the key tuple and the cred is the
385 | OAuth2Credential object.
386 | """
387 | raw_key = cred_entry['key']
388 | key = util.dict_to_tuple_key(raw_key)
389 | credential = None
390 | credential = Credentials.new_from_json(simplejson.dumps(cred_entry['credential']))
391 | return (key, credential)
392 |
393 | def _write(self):
394 | """Write the cached data back out.
395 |
396 | The multistore must be locked.
397 | """
398 | raw_data = {'file_version': 1}
399 | raw_creds = []
400 | raw_data['data'] = raw_creds
401 | for (cred_key, cred) in self._data.items():
402 | raw_key = dict(cred_key)
403 | raw_cred = simplejson.loads(cred.to_json())
404 | raw_creds.append({'key': raw_key, 'credential': raw_cred})
405 | self._locked_json_write(raw_data)
406 |
407 | def _get_all_credential_keys(self):
408 | """Gets all the registered credential keys in the multistore.
409 |
410 | Returns:
411 | A list of dictionaries corresponding to all the keys currently registered
412 | """
413 | return [dict(key) for key in self._data.keys()]
414 |
415 | def _get_credential(self, key):
416 | """Get a credential from the multistore.
417 |
418 | The multistore must be locked.
419 |
420 | Args:
421 | key: The key used to retrieve the credential
422 |
423 | Returns:
424 | The credential specified or None if not present
425 | """
426 | return self._data.get(key, None)
427 |
428 | def _update_credential(self, key, cred):
429 | """Update a credential and write the multistore.
430 |
431 | This must be called when the multistore is locked.
432 |
433 | Args:
434 | key: The key used to retrieve the credential
435 | cred: The OAuth2Credential to update/set
436 | """
437 | self._data[key] = cred
438 | self._write()
439 |
440 | def _delete_credential(self, key):
441 | """Delete a credential and write the multistore.
442 |
443 | This must be called when the multistore is locked.
444 |
445 | Args:
446 | key: The key used to retrieve the credential
447 | """
448 | try:
449 | del self._data[key]
450 | except KeyError:
451 | pass
452 | self._write()
453 |
454 | def _get_storage(self, key):
455 | """Get a Storage object to get/set a credential.
456 |
457 | This Storage is a 'view' into the multistore.
458 |
459 | Args:
460 | key: The key used to retrieve the credential
461 |
462 | Returns:
463 | A Storage object that can be used to get/set this cred
464 | """
465 | return self._Storage(self, key)
466 |
--------------------------------------------------------------------------------
/httplib2/socks.py:
--------------------------------------------------------------------------------
1 | """SocksiPy - Python SOCKS module.
2 | Version 1.00
3 |
4 | Copyright 2006 Dan-Haim. All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 | 1. Redistributions of source code must retain the above copyright notice, this
9 | list of conditions and the following disclaimer.
10 | 2. Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 | 3. Neither the name of Dan Haim nor the names of his contributors may be used
14 | to endorse or promote products derived from this software without specific
15 | prior written permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
18 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
19 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
20 | EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
21 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
23 | OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
25 | OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
26 |
27 |
28 | This module provides a standard socket-like interface for Python
29 | for tunneling connections through SOCKS proxies.
30 |
31 | """
32 |
33 | """
34 |
35 | Minor modifications made by Christopher Gilbert (http://motomastyle.com/)
36 | for use in PyLoris (http://pyloris.sourceforge.net/)
37 |
38 | Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/)
39 | mainly to merge bug fixes found in Sourceforge
40 |
41 | """
42 |
43 | import base64
44 | import socket
45 | import struct
46 | import sys
47 |
48 | if getattr(socket, 'socket', None) is None:
49 | raise ImportError('socket.socket missing, proxy support unusable')
50 |
51 | PROXY_TYPE_SOCKS4 = 1
52 | PROXY_TYPE_SOCKS5 = 2
53 | PROXY_TYPE_HTTP = 3
54 | PROXY_TYPE_HTTP_NO_TUNNEL = 4
55 |
56 | _defaultproxy = None
57 | _orgsocket = socket.socket
58 |
59 | class ProxyError(Exception): pass
60 | class GeneralProxyError(ProxyError): pass
61 | class Socks5AuthError(ProxyError): pass
62 | class Socks5Error(ProxyError): pass
63 | class Socks4Error(ProxyError): pass
64 | class HTTPError(ProxyError): pass
65 |
66 | _generalerrors = ("success",
67 | "invalid data",
68 | "not connected",
69 | "not available",
70 | "bad proxy type",
71 | "bad input")
72 |
73 | _socks5errors = ("succeeded",
74 | "general SOCKS server failure",
75 | "connection not allowed by ruleset",
76 | "Network unreachable",
77 | "Host unreachable",
78 | "Connection refused",
79 | "TTL expired",
80 | "Command not supported",
81 | "Address type not supported",
82 | "Unknown error")
83 |
84 | _socks5autherrors = ("succeeded",
85 | "authentication is required",
86 | "all offered authentication methods were rejected",
87 | "unknown username or invalid password",
88 | "unknown error")
89 |
90 | _socks4errors = ("request granted",
91 | "request rejected or failed",
92 | "request rejected because SOCKS server cannot connect to identd on the client",
93 | "request rejected because the client program and identd report different user-ids",
94 | "unknown error")
95 |
96 | def setdefaultproxy(proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
97 | """setdefaultproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
98 | Sets a default proxy which all further socksocket objects will use,
99 | unless explicitly changed.
100 | """
101 | global _defaultproxy
102 | _defaultproxy = (proxytype, addr, port, rdns, username, password)
103 |
104 | def wrapmodule(module):
105 | """wrapmodule(module)
106 | Attempts to replace a module's socket library with a SOCKS socket. Must set
107 | a default proxy using setdefaultproxy(...) first.
108 | This will only work on modules that import socket directly into the namespace;
109 | most of the Python Standard Library falls into this category.
110 | """
111 | if _defaultproxy != None:
112 | module.socket.socket = socksocket
113 | else:
114 | raise GeneralProxyError((4, "no proxy specified"))
115 |
116 | class socksocket(socket.socket):
117 | """socksocket([family[, type[, proto]]]) -> socket object
118 | Open a SOCKS enabled socket. The parameters are the same as
119 | those of the standard socket init. In order for SOCKS to work,
120 | you must specify family=AF_INET, type=SOCK_STREAM and proto=0.
121 | """
122 |
123 | def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0, _sock=None):
124 | _orgsocket.__init__(self, family, type, proto, _sock)
125 | if _defaultproxy != None:
126 | self.__proxy = _defaultproxy
127 | else:
128 | self.__proxy = (None, None, None, None, None, None)
129 | self.__proxysockname = None
130 | self.__proxypeername = None
131 | self.__httptunnel = True
132 |
133 | def __recvall(self, count):
134 | """__recvall(count) -> data
135 | Receive EXACTLY the number of bytes requested from the socket.
136 | Blocks until the required number of bytes have been received.
137 | """
138 | data = self.recv(count)
139 | while len(data) < count:
140 | d = self.recv(count-len(data))
141 | if not d: raise GeneralProxyError((0, "connection closed unexpectedly"))
142 | data = data + d
143 | return data
144 |
145 | def sendall(self, content, *args):
146 | """ override socket.socket.sendall method to rewrite the header
147 | for non-tunneling proxies if needed
148 | """
149 | if not self.__httptunnel:
150 | content = self.__rewriteproxy(content)
151 | return super(socksocket, self).sendall(content, *args)
152 |
153 | def __rewriteproxy(self, header):
154 | """ rewrite HTTP request headers to support non-tunneling proxies
155 | (i.e. those which do not support the CONNECT method).
156 | This only works for HTTP (not HTTPS) since HTTPS requires tunneling.
157 | """
158 | host, endpt = None, None
159 | hdrs = header.split("\r\n")
160 | for hdr in hdrs:
161 | if hdr.lower().startswith("host:"):
162 | host = hdr
163 | elif hdr.lower().startswith("get") or hdr.lower().startswith("post"):
164 | endpt = hdr
165 | if host and endpt:
166 | hdrs.remove(host)
167 | hdrs.remove(endpt)
168 | host = host.split(" ")[1]
169 | endpt = endpt.split(" ")
170 | if (self.__proxy[4] != None and self.__proxy[5] != None):
171 | hdrs.insert(0, self.__getauthheader())
172 | hdrs.insert(0, "Host: %s" % host)
173 | hdrs.insert(0, "%s http://%s%s %s" % (endpt[0], host, endpt[1], endpt[2]))
174 | return "\r\n".join(hdrs)
175 |
176 | def __getauthheader(self):
177 | auth = self.__proxy[4] + ":" + self.__proxy[5]
178 | return "Proxy-Authorization: Basic " + base64.b64encode(auth)
179 |
180 | def setproxy(self, proxytype=None, addr=None, port=None, rdns=True, username=None, password=None):
181 | """setproxy(proxytype, addr[, port[, rdns[, username[, password]]]])
182 | Sets the proxy to be used.
183 | proxytype - The type of the proxy to be used. Three types
184 | are supported: PROXY_TYPE_SOCKS4 (including socks4a),
185 | PROXY_TYPE_SOCKS5 and PROXY_TYPE_HTTP
186 | addr - The address of the server (IP or DNS).
187 | port - The port of the server. Defaults to 1080 for SOCKS
188 | servers and 8080 for HTTP proxy servers.
189 | rdns - Should DNS queries be preformed on the remote side
190 | (rather than the local side). The default is True.
191 | Note: This has no effect with SOCKS4 servers.
192 | username - Username to authenticate with to the server.
193 | The default is no authentication.
194 | password - Password to authenticate with to the server.
195 | Only relevant when username is also provided.
196 | """
197 | self.__proxy = (proxytype, addr, port, rdns, username, password)
198 |
199 | def __negotiatesocks5(self, destaddr, destport):
200 | """__negotiatesocks5(self,destaddr,destport)
201 | Negotiates a connection through a SOCKS5 server.
202 | """
203 | # First we'll send the authentication packages we support.
204 | if (self.__proxy[4]!=None) and (self.__proxy[5]!=None):
205 | # The username/password details were supplied to the
206 | # setproxy method so we support the USERNAME/PASSWORD
207 | # authentication (in addition to the standard none).
208 | self.sendall(struct.pack('BBBB', 0x05, 0x02, 0x00, 0x02))
209 | else:
210 | # No username/password were entered, therefore we
211 | # only support connections with no authentication.
212 | self.sendall(struct.pack('BBB', 0x05, 0x01, 0x00))
213 | # We'll receive the server's response to determine which
214 | # method was selected
215 | chosenauth = self.__recvall(2)
216 | if chosenauth[0:1] != chr(0x05).encode():
217 | self.close()
218 | raise GeneralProxyError((1, _generalerrors[1]))
219 | # Check the chosen authentication method
220 | if chosenauth[1:2] == chr(0x00).encode():
221 | # No authentication is required
222 | pass
223 | elif chosenauth[1:2] == chr(0x02).encode():
224 | # Okay, we need to perform a basic username/password
225 | # authentication.
226 | self.sendall(chr(0x01).encode() + chr(len(self.__proxy[4])) + self.__proxy[4] + chr(len(self.__proxy[5])) + self.__proxy[5])
227 | authstat = self.__recvall(2)
228 | if authstat[0:1] != chr(0x01).encode():
229 | # Bad response
230 | self.close()
231 | raise GeneralProxyError((1, _generalerrors[1]))
232 | if authstat[1:2] != chr(0x00).encode():
233 | # Authentication failed
234 | self.close()
235 | raise Socks5AuthError((3, _socks5autherrors[3]))
236 | # Authentication succeeded
237 | else:
238 | # Reaching here is always bad
239 | self.close()
240 | if chosenauth[1] == chr(0xFF).encode():
241 | raise Socks5AuthError((2, _socks5autherrors[2]))
242 | else:
243 | raise GeneralProxyError((1, _generalerrors[1]))
244 | # Now we can request the actual connection
245 | req = struct.pack('BBB', 0x05, 0x01, 0x00)
246 | # If the given destination address is an IP address, we'll
247 | # use the IPv4 address request even if remote resolving was specified.
248 | try:
249 | ipaddr = socket.inet_aton(destaddr)
250 | req = req + chr(0x01).encode() + ipaddr
251 | except socket.error:
252 | # Well it's not an IP number, so it's probably a DNS name.
253 | if self.__proxy[3]:
254 | # Resolve remotely
255 | ipaddr = None
256 | req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr
257 | else:
258 | # Resolve locally
259 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
260 | req = req + chr(0x01).encode() + ipaddr
261 | req = req + struct.pack(">H", destport)
262 | self.sendall(req)
263 | # Get the response
264 | resp = self.__recvall(4)
265 | if resp[0:1] != chr(0x05).encode():
266 | self.close()
267 | raise GeneralProxyError((1, _generalerrors[1]))
268 | elif resp[1:2] != chr(0x00).encode():
269 | # Connection failed
270 | self.close()
271 | if ord(resp[1:2])<=8:
272 | raise Socks5Error((ord(resp[1:2]), _socks5errors[ord(resp[1:2])]))
273 | else:
274 | raise Socks5Error((9, _socks5errors[9]))
275 | # Get the bound address/port
276 | elif resp[3:4] == chr(0x01).encode():
277 | boundaddr = self.__recvall(4)
278 | elif resp[3:4] == chr(0x03).encode():
279 | resp = resp + self.recv(1)
280 | boundaddr = self.__recvall(ord(resp[4:5]))
281 | else:
282 | self.close()
283 | raise GeneralProxyError((1,_generalerrors[1]))
284 | boundport = struct.unpack(">H", self.__recvall(2))[0]
285 | self.__proxysockname = (boundaddr, boundport)
286 | if ipaddr != None:
287 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
288 | else:
289 | self.__proxypeername = (destaddr, destport)
290 |
291 | def getproxysockname(self):
292 | """getsockname() -> address info
293 | Returns the bound IP address and port number at the proxy.
294 | """
295 | return self.__proxysockname
296 |
297 | def getproxypeername(self):
298 | """getproxypeername() -> address info
299 | Returns the IP and port number of the proxy.
300 | """
301 | return _orgsocket.getpeername(self)
302 |
303 | def getpeername(self):
304 | """getpeername() -> address info
305 | Returns the IP address and port number of the destination
306 | machine (note: getproxypeername returns the proxy)
307 | """
308 | return self.__proxypeername
309 |
310 | def __negotiatesocks4(self,destaddr,destport):
311 | """__negotiatesocks4(self,destaddr,destport)
312 | Negotiates a connection through a SOCKS4 server.
313 | """
314 | # Check if the destination address provided is an IP address
315 | rmtrslv = False
316 | try:
317 | ipaddr = socket.inet_aton(destaddr)
318 | except socket.error:
319 | # It's a DNS name. Check where it should be resolved.
320 | if self.__proxy[3]:
321 | ipaddr = struct.pack("BBBB", 0x00, 0x00, 0x00, 0x01)
322 | rmtrslv = True
323 | else:
324 | ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))
325 | # Construct the request packet
326 | req = struct.pack(">BBH", 0x04, 0x01, destport) + ipaddr
327 | # The username parameter is considered userid for SOCKS4
328 | if self.__proxy[4] != None:
329 | req = req + self.__proxy[4]
330 | req = req + chr(0x00).encode()
331 | # DNS name if remote resolving is required
332 | # NOTE: This is actually an extension to the SOCKS4 protocol
333 | # called SOCKS4A and may not be supported in all cases.
334 | if rmtrslv:
335 | req = req + destaddr + chr(0x00).encode()
336 | self.sendall(req)
337 | # Get the response from the server
338 | resp = self.__recvall(8)
339 | if resp[0:1] != chr(0x00).encode():
340 | # Bad data
341 | self.close()
342 | raise GeneralProxyError((1,_generalerrors[1]))
343 | if resp[1:2] != chr(0x5A).encode():
344 | # Server returned an error
345 | self.close()
346 | if ord(resp[1:2]) in (91, 92, 93):
347 | self.close()
348 | raise Socks4Error((ord(resp[1:2]), _socks4errors[ord(resp[1:2]) - 90]))
349 | else:
350 | raise Socks4Error((94, _socks4errors[4]))
351 | # Get the bound address/port
352 | self.__proxysockname = (socket.inet_ntoa(resp[4:]), struct.unpack(">H", resp[2:4])[0])
353 | if rmtrslv != None:
354 | self.__proxypeername = (socket.inet_ntoa(ipaddr), destport)
355 | else:
356 | self.__proxypeername = (destaddr, destport)
357 |
358 | def __negotiatehttp(self, destaddr, destport):
359 | """__negotiatehttp(self,destaddr,destport)
360 | Negotiates a connection through an HTTP server.
361 | """
362 | # If we need to resolve locally, we do this now
363 | if not self.__proxy[3]:
364 | addr = socket.gethostbyname(destaddr)
365 | else:
366 | addr = destaddr
367 | headers = ["CONNECT ", addr, ":", str(destport), " HTTP/1.1\r\n"]
368 | headers += ["Host: ", destaddr, "\r\n"]
369 | if (self.__proxy[4] != None and self.__proxy[5] != None):
370 | headers += [self.__getauthheader(), "\r\n"]
371 | headers.append("\r\n")
372 | self.sendall("".join(headers).encode())
373 | # We read the response until we get the string "\r\n\r\n"
374 | resp = self.recv(1)
375 | while resp.find("\r\n\r\n".encode()) == -1:
376 | resp = resp + self.recv(1)
377 | # We just need the first line to check if the connection
378 | # was successful
379 | statusline = resp.splitlines()[0].split(" ".encode(), 2)
380 | if statusline[0] not in ("HTTP/1.0".encode(), "HTTP/1.1".encode()):
381 | self.close()
382 | raise GeneralProxyError((1, _generalerrors[1]))
383 | try:
384 | statuscode = int(statusline[1])
385 | except ValueError:
386 | self.close()
387 | raise GeneralProxyError((1, _generalerrors[1]))
388 | if statuscode != 200:
389 | self.close()
390 | raise HTTPError((statuscode, statusline[2]))
391 | self.__proxysockname = ("0.0.0.0", 0)
392 | self.__proxypeername = (addr, destport)
393 |
394 | def connect(self, destpair):
395 | """connect(self, despair)
396 | Connects to the specified destination through a proxy.
397 | destpar - A tuple of the IP/DNS address and the port number.
398 | (identical to socket's connect).
399 | To select the proxy server use setproxy().
400 | """
401 | # Do a minimal input check first
402 | if (not type(destpair) in (list,tuple)) or (len(destpair) < 2) or (not isinstance(destpair[0], basestring)) or (type(destpair[1]) != int):
403 | raise GeneralProxyError((5, _generalerrors[5]))
404 | if self.__proxy[0] == PROXY_TYPE_SOCKS5:
405 | if self.__proxy[2] != None:
406 | portnum = self.__proxy[2]
407 | else:
408 | portnum = 1080
409 | _orgsocket.connect(self, (self.__proxy[1], portnum))
410 | self.__negotiatesocks5(destpair[0], destpair[1])
411 | elif self.__proxy[0] == PROXY_TYPE_SOCKS4:
412 | if self.__proxy[2] != None:
413 | portnum = self.__proxy[2]
414 | else:
415 | portnum = 1080
416 | _orgsocket.connect(self,(self.__proxy[1], portnum))
417 | self.__negotiatesocks4(destpair[0], destpair[1])
418 | elif self.__proxy[0] == PROXY_TYPE_HTTP:
419 | if self.__proxy[2] != None:
420 | portnum = self.__proxy[2]
421 | else:
422 | portnum = 8080
423 | _orgsocket.connect(self,(self.__proxy[1], portnum))
424 | self.__negotiatehttp(destpair[0], destpair[1])
425 | elif self.__proxy[0] == PROXY_TYPE_HTTP_NO_TUNNEL:
426 | if self.__proxy[2] != None:
427 | portnum = self.__proxy[2]
428 | else:
429 | portnum = 8080
430 | _orgsocket.connect(self,(self.__proxy[1],portnum))
431 | if destpair[1] == 443:
432 | self.__negotiatehttp(destpair[0],destpair[1])
433 | else:
434 | self.__httptunnel = False
435 | elif self.__proxy[0] == None:
436 | _orgsocket.connect(self, (destpair[0], destpair[1]))
437 | else:
438 | raise GeneralProxyError((4, _generalerrors[4]))
439 |
--------------------------------------------------------------------------------
/oauth2client/appengine.py:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2010 Google Inc.
2 | #
3 | # Licensed under the Apache License, Version 2.0 (the "License");
4 | # you may not use this file except in compliance with the License.
5 | # You may obtain a copy of the License at
6 | #
7 | # http://www.apache.org/licenses/LICENSE-2.0
8 | #
9 | # Unless required by applicable law or agreed to in writing, software
10 | # distributed under the License is distributed on an "AS IS" BASIS,
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | # See the License for the specific language governing permissions and
13 | # limitations under the License.
14 |
15 | """Utilities for Google App Engine
16 |
17 | Utilities for making it easier to use OAuth 2.0 on Google App Engine.
18 | """
19 |
20 | __author__ = 'jcgregorio@google.com (Joe Gregorio)'
21 |
22 | import base64
23 | import cgi
24 | import httplib2
25 | import logging
26 | import os
27 | import pickle
28 | import threading
29 | import time
30 |
31 | from google.appengine.api import app_identity
32 | from google.appengine.api import memcache
33 | from google.appengine.api import users
34 | from google.appengine.ext import db
35 | from google.appengine.ext import webapp
36 | from google.appengine.ext.webapp.util import login_required
37 | from google.appengine.ext.webapp.util import run_wsgi_app
38 | from oauth2client import GOOGLE_AUTH_URI
39 | from oauth2client import GOOGLE_REVOKE_URI
40 | from oauth2client import GOOGLE_TOKEN_URI
41 | from oauth2client import clientsecrets
42 | from oauth2client import util
43 | from oauth2client import xsrfutil
44 | from oauth2client.anyjson import simplejson
45 | from oauth2client.client import AccessTokenRefreshError
46 | from oauth2client.client import AssertionCredentials
47 | from oauth2client.client import Credentials
48 | from oauth2client.client import Flow
49 | from oauth2client.client import OAuth2WebServerFlow
50 | from oauth2client.client import Storage
51 |
52 | # TODO(dhermes): Resolve import issue.
53 | # This is a temporary fix for a Google internal issue.
54 | try:
55 | from google.appengine.ext import ndb
56 | except ImportError:
57 | ndb = None
58 |
59 |
60 | logger = logging.getLogger(__name__)
61 |
62 | OAUTH2CLIENT_NAMESPACE = 'oauth2client#ns'
63 |
64 | XSRF_MEMCACHE_ID = 'xsrf_secret_key'
65 |
66 |
67 | def _safe_html(s):
68 | """Escape text to make it safe to display.
69 |
70 | Args:
71 | s: string, The text to escape.
72 |
73 | Returns:
74 | The escaped text as a string.
75 | """
76 | return cgi.escape(s, quote=1).replace("'", ''')
77 |
78 |
79 | class InvalidClientSecretsError(Exception):
80 | """The client_secrets.json file is malformed or missing required fields."""
81 |
82 |
83 | class InvalidXsrfTokenError(Exception):
84 | """The XSRF token is invalid or expired."""
85 |
86 |
87 | class SiteXsrfSecretKey(db.Model):
88 | """Storage for the sites XSRF secret key.
89 |
90 | There will only be one instance stored of this model, the one used for the
91 | site.
92 | """
93 | secret = db.StringProperty()
94 |
95 | if ndb is not None:
96 | class SiteXsrfSecretKeyNDB(ndb.Model):
97 | """NDB Model for storage for the sites XSRF secret key.
98 |
99 | Since this model uses the same kind as SiteXsrfSecretKey, it can be used
100 | interchangeably. This simply provides an NDB model for interacting with the
101 | same data the DB model interacts with.
102 |
103 | There should only be one instance stored of this model, the one used for the
104 | site.
105 | """
106 | secret = ndb.StringProperty()
107 |
108 | @classmethod
109 | def _get_kind(cls):
110 | """Return the kind name for this class."""
111 | return 'SiteXsrfSecretKey'
112 |
113 |
114 | def _generate_new_xsrf_secret_key():
115 | """Returns a random XSRF secret key.
116 | """
117 | return os.urandom(16).encode("hex")
118 |
119 |
120 | def xsrf_secret_key():
121 | """Return the secret key for use for XSRF protection.
122 |
123 | If the Site entity does not have a secret key, this method will also create
124 | one and persist it.
125 |
126 | Returns:
127 | The secret key.
128 | """
129 | secret = memcache.get(XSRF_MEMCACHE_ID, namespace=OAUTH2CLIENT_NAMESPACE)
130 | if not secret:
131 | # Load the one and only instance of SiteXsrfSecretKey.
132 | model = SiteXsrfSecretKey.get_or_insert(key_name='site')
133 | if not model.secret:
134 | model.secret = _generate_new_xsrf_secret_key()
135 | model.put()
136 | secret = model.secret
137 | memcache.add(XSRF_MEMCACHE_ID, secret, namespace=OAUTH2CLIENT_NAMESPACE)
138 |
139 | return str(secret)
140 |
141 |
142 | class AppAssertionCredentials(AssertionCredentials):
143 | """Credentials object for App Engine Assertion Grants
144 |
145 | This object will allow an App Engine application to identify itself to Google
146 | and other OAuth 2.0 servers that can verify assertions. It can be used for the
147 | purpose of accessing data stored under an account assigned to the App Engine
148 | application itself.
149 |
150 | This credential does not require a flow to instantiate because it represents
151 | a two legged flow, and therefore has all of the required information to
152 | generate and refresh its own access tokens.
153 | """
154 |
155 | @util.positional(2)
156 | def __init__(self, scope, **kwargs):
157 | """Constructor for AppAssertionCredentials
158 |
159 | Args:
160 | scope: string or iterable of strings, scope(s) of the credentials being
161 | requested.
162 | """
163 | self.scope = util.scopes_to_string(scope)
164 |
165 | # Assertion type is no longer used, but still in the parent class signature.
166 | super(AppAssertionCredentials, self).__init__(None)
167 |
168 | @classmethod
169 | def from_json(cls, json):
170 | data = simplejson.loads(json)
171 | return AppAssertionCredentials(data['scope'])
172 |
173 | def _refresh(self, http_request):
174 | """Refreshes the access_token.
175 |
176 | Since the underlying App Engine app_identity implementation does its own
177 | caching we can skip all the storage hoops and just to a refresh using the
178 | API.
179 |
180 | Args:
181 | http_request: callable, a callable that matches the method signature of
182 | httplib2.Http.request, used to make the refresh request.
183 |
184 | Raises:
185 | AccessTokenRefreshError: When the refresh fails.
186 | """
187 | try:
188 | scopes = self.scope.split()
189 | (token, _) = app_identity.get_access_token(scopes)
190 | except app_identity.Error, e:
191 | raise AccessTokenRefreshError(str(e))
192 | self.access_token = token
193 |
194 |
195 | class FlowProperty(db.Property):
196 | """App Engine datastore Property for Flow.
197 |
198 | Utility property that allows easy storage and retrieval of an
199 | oauth2client.Flow"""
200 |
201 | # Tell what the user type is.
202 | data_type = Flow
203 |
204 | # For writing to datastore.
205 | def get_value_for_datastore(self, model_instance):
206 | flow = super(FlowProperty,
207 | self).get_value_for_datastore(model_instance)
208 | return db.Blob(pickle.dumps(flow))
209 |
210 | # For reading from datastore.
211 | def make_value_from_datastore(self, value):
212 | if value is None:
213 | return None
214 | return pickle.loads(value)
215 |
216 | def validate(self, value):
217 | if value is not None and not isinstance(value, Flow):
218 | raise db.BadValueError('Property %s must be convertible '
219 | 'to a FlowThreeLegged instance (%s)' %
220 | (self.name, value))
221 | return super(FlowProperty, self).validate(value)
222 |
223 | def empty(self, value):
224 | return not value
225 |
226 |
227 | if ndb is not None:
228 | class FlowNDBProperty(ndb.PickleProperty):
229 | """App Engine NDB datastore Property for Flow.
230 |
231 | Serves the same purpose as the DB FlowProperty, but for NDB models. Since
232 | PickleProperty inherits from BlobProperty, the underlying representation of
233 | the data in the datastore will be the same as in the DB case.
234 |
235 | Utility property that allows easy storage and retrieval of an
236 | oauth2client.Flow
237 | """
238 |
239 | def _validate(self, value):
240 | """Validates a value as a proper Flow object.
241 |
242 | Args:
243 | value: A value to be set on the property.
244 |
245 | Raises:
246 | TypeError if the value is not an instance of Flow.
247 | """
248 | logger.info('validate: Got type %s', type(value))
249 | if value is not None and not isinstance(value, Flow):
250 | raise TypeError('Property %s must be convertible to a flow '
251 | 'instance; received: %s.' % (self._name, value))
252 |
253 |
254 | class CredentialsProperty(db.Property):
255 | """App Engine datastore Property for Credentials.
256 |
257 | Utility property that allows easy storage and retrieval of
258 | oath2client.Credentials
259 | """
260 |
261 | # Tell what the user type is.
262 | data_type = Credentials
263 |
264 | # For writing to datastore.
265 | def get_value_for_datastore(self, model_instance):
266 | logger.info("get: Got type " + str(type(model_instance)))
267 | cred = super(CredentialsProperty,
268 | self).get_value_for_datastore(model_instance)
269 | if cred is None:
270 | cred = ''
271 | else:
272 | cred = cred.to_json()
273 | return db.Blob(cred)
274 |
275 | # For reading from datastore.
276 | def make_value_from_datastore(self, value):
277 | logger.info("make: Got type " + str(type(value)))
278 | if value is None:
279 | return None
280 | if len(value) == 0:
281 | return None
282 | try:
283 | credentials = Credentials.new_from_json(value)
284 | except ValueError:
285 | credentials = None
286 | return credentials
287 |
288 | def validate(self, value):
289 | value = super(CredentialsProperty, self).validate(value)
290 | logger.info("validate: Got type " + str(type(value)))
291 | if value is not None and not isinstance(value, Credentials):
292 | raise db.BadValueError('Property %s must be convertible '
293 | 'to a Credentials instance (%s)' %
294 | (self.name, value))
295 | #if value is not None and not isinstance(value, Credentials):
296 | # return None
297 | return value
298 |
299 |
300 | if ndb is not None:
301 | # TODO(dhermes): Turn this into a JsonProperty and overhaul the Credentials
302 | # and subclass mechanics to use new_from_dict, to_dict,
303 | # from_dict, etc.
304 | class CredentialsNDBProperty(ndb.BlobProperty):
305 | """App Engine NDB datastore Property for Credentials.
306 |
307 | Serves the same purpose as the DB CredentialsProperty, but for NDB models.
308 | Since CredentialsProperty stores data as a blob and this inherits from
309 | BlobProperty, the data in the datastore will be the same as in the DB case.
310 |
311 | Utility property that allows easy storage and retrieval of Credentials and
312 | subclasses.
313 | """
314 | def _validate(self, value):
315 | """Validates a value as a proper credentials object.
316 |
317 | Args:
318 | value: A value to be set on the property.
319 |
320 | Raises:
321 | TypeError if the value is not an instance of Credentials.
322 | """
323 | logger.info('validate: Got type %s', type(value))
324 | if value is not None and not isinstance(value, Credentials):
325 | raise TypeError('Property %s must be convertible to a credentials '
326 | 'instance; received: %s.' % (self._name, value))
327 |
328 | def _to_base_type(self, value):
329 | """Converts our validated value to a JSON serialized string.
330 |
331 | Args:
332 | value: A value to be set in the datastore.
333 |
334 | Returns:
335 | A JSON serialized version of the credential, else '' if value is None.
336 | """
337 | if value is None:
338 | return ''
339 | else:
340 | return value.to_json()
341 |
342 | def _from_base_type(self, value):
343 | """Converts our stored JSON string back to the desired type.
344 |
345 | Args:
346 | value: A value from the datastore to be converted to the desired type.
347 |
348 | Returns:
349 | A deserialized Credentials (or subclass) object, else None if the
350 | value can't be parsed.
351 | """
352 | if not value:
353 | return None
354 | try:
355 | # Uses the from_json method of the implied class of value
356 | credentials = Credentials.new_from_json(value)
357 | except ValueError:
358 | credentials = None
359 | return credentials
360 |
361 |
362 | class StorageByKeyName(Storage):
363 | """Store and retrieve a credential to and from the App Engine datastore.
364 |
365 | This Storage helper presumes the Credentials have been stored as a
366 | CredentialsProperty or CredentialsNDBProperty on a datastore model class, and
367 | that entities are stored by key_name.
368 | """
369 |
370 | @util.positional(4)
371 | def __init__(self, model, key_name, property_name, cache=None, user=None):
372 | """Constructor for Storage.
373 |
374 | Args:
375 | model: db.Model or ndb.Model, model class
376 | key_name: string, key name for the entity that has the credentials
377 | property_name: string, name of the property that is a CredentialsProperty
378 | or CredentialsNDBProperty.
379 | cache: memcache, a write-through cache to put in front of the datastore.
380 | If the model you are using is an NDB model, using a cache will be
381 | redundant since the model uses an instance cache and memcache for you.
382 | user: users.User object, optional. Can be used to grab user ID as a
383 | key_name if no key name is specified.
384 | """
385 | if key_name is None:
386 | if user is None:
387 | raise ValueError('StorageByKeyName called with no key name or user.')
388 | key_name = user.user_id()
389 |
390 | self._model = model
391 | self._key_name = key_name
392 | self._property_name = property_name
393 | self._cache = cache
394 |
395 | def _is_ndb(self):
396 | """Determine whether the model of the instance is an NDB model.
397 |
398 | Returns:
399 | Boolean indicating whether or not the model is an NDB or DB model.
400 | """
401 | # issubclass will fail if one of the arguments is not a class, only need
402 | # worry about new-style classes since ndb and db models are new-style
403 | if isinstance(self._model, type):
404 | if ndb is not None and issubclass(self._model, ndb.Model):
405 | return True
406 | elif issubclass(self._model, db.Model):
407 | return False
408 |
409 | raise TypeError('Model class not an NDB or DB model: %s.' % (self._model,))
410 |
411 | def _get_entity(self):
412 | """Retrieve entity from datastore.
413 |
414 | Uses a different model method for db or ndb models.
415 |
416 | Returns:
417 | Instance of the model corresponding to the current storage object
418 | and stored using the key name of the storage object.
419 | """
420 | if self._is_ndb():
421 | return self._model.get_by_id(self._key_name)
422 | else:
423 | return self._model.get_by_key_name(self._key_name)
424 |
425 | def _delete_entity(self):
426 | """Delete entity from datastore.
427 |
428 | Attempts to delete using the key_name stored on the object, whether or not
429 | the given key is in the datastore.
430 | """
431 | if self._is_ndb():
432 | ndb.Key(self._model, self._key_name).delete()
433 | else:
434 | entity_key = db.Key.from_path(self._model.kind(), self._key_name)
435 | db.delete(entity_key)
436 |
437 | def locked_get(self):
438 | """Retrieve Credential from datastore.
439 |
440 | Returns:
441 | oauth2client.Credentials
442 | """
443 | credentials = None
444 | if self._cache:
445 | json = self._cache.get(self._key_name)
446 | if json:
447 | credentials = Credentials.new_from_json(json)
448 | if credentials is None:
449 | entity = self._get_entity()
450 | if entity is not None:
451 | credentials = getattr(entity, self._property_name)
452 | if self._cache:
453 | self._cache.set(self._key_name, credentials.to_json())
454 |
455 | if credentials and hasattr(credentials, 'set_store'):
456 | credentials.set_store(self)
457 | return credentials
458 |
459 | def locked_put(self, credentials):
460 | """Write a Credentials to the datastore.
461 |
462 | Args:
463 | credentials: Credentials, the credentials to store.
464 | """
465 | entity = self._model.get_or_insert(self._key_name)
466 | setattr(entity, self._property_name, credentials)
467 | entity.put()
468 | if self._cache:
469 | self._cache.set(self._key_name, credentials.to_json())
470 |
471 | def locked_delete(self):
472 | """Delete Credential from datastore."""
473 |
474 | if self._cache:
475 | self._cache.delete(self._key_name)
476 |
477 | self._delete_entity()
478 |
479 |
480 | class CredentialsModel(db.Model):
481 | """Storage for OAuth 2.0 Credentials
482 |
483 | Storage of the model is keyed by the user.user_id().
484 | """
485 | credentials = CredentialsProperty()
486 |
487 |
488 | if ndb is not None:
489 | class CredentialsNDBModel(ndb.Model):
490 | """NDB Model for storage of OAuth 2.0 Credentials
491 |
492 | Since this model uses the same kind as CredentialsModel and has a property
493 | which can serialize and deserialize Credentials correctly, it can be used
494 | interchangeably with a CredentialsModel to access, insert and delete the
495 | same entities. This simply provides an NDB model for interacting with the
496 | same data the DB model interacts with.
497 |
498 | Storage of the model is keyed by the user.user_id().
499 | """
500 | credentials = CredentialsNDBProperty()
501 |
502 | @classmethod
503 | def _get_kind(cls):
504 | """Return the kind name for this class."""
505 | return 'CredentialsModel'
506 |
507 |
508 | def _build_state_value(request_handler, user):
509 | """Composes the value for the 'state' parameter.
510 |
511 | Packs the current request URI and an XSRF token into an opaque string that
512 | can be passed to the authentication server via the 'state' parameter.
513 |
514 | Args:
515 | request_handler: webapp.RequestHandler, The request.
516 | user: google.appengine.api.users.User, The current user.
517 |
518 | Returns:
519 | The state value as a string.
520 | """
521 | uri = request_handler.request.url
522 | token = xsrfutil.generate_token(xsrf_secret_key(), user.user_id(),
523 | action_id=str(uri))
524 | return uri + ':' + token
525 |
526 |
527 | def _parse_state_value(state, user):
528 | """Parse the value of the 'state' parameter.
529 |
530 | Parses the value and validates the XSRF token in the state parameter.
531 |
532 | Args:
533 | state: string, The value of the state parameter.
534 | user: google.appengine.api.users.User, The current user.
535 |
536 | Raises:
537 | InvalidXsrfTokenError: if the XSRF token is invalid.
538 |
539 | Returns:
540 | The redirect URI.
541 | """
542 | uri, token = state.rsplit(':', 1)
543 | if not xsrfutil.validate_token(xsrf_secret_key(), token, user.user_id(),
544 | action_id=uri):
545 | raise InvalidXsrfTokenError()
546 |
547 | return uri
548 |
549 |
550 | class OAuth2Decorator(object):
551 | """Utility for making OAuth 2.0 easier.
552 |
553 | Instantiate and then use with oauth_required or oauth_aware
554 | as decorators on webapp.RequestHandler methods.
555 |
556 | Example:
557 |
558 | decorator = OAuth2Decorator(
559 | client_id='837...ent.com',
560 | client_secret='Qh...wwI',
561 | scope='https://www.googleapis.com/auth/plus')
562 |
563 |
564 | class MainHandler(webapp.RequestHandler):
565 |
566 | @decorator.oauth_required
567 | def get(self):
568 | http = decorator.http()
569 | # http is authorized with the user's Credentials and can be used
570 | # in API calls
571 |
572 | """
573 |
574 | def set_credentials(self, credentials):
575 | self._tls.credentials = credentials
576 |
577 | def get_credentials(self):
578 | """A thread local Credentials object.
579 |
580 | Returns:
581 | A client.Credentials object, or None if credentials hasn't been set in
582 | this thread yet, which may happen when calling has_credentials inside
583 | oauth_aware.
584 | """
585 | return getattr(self._tls, 'credentials', None)
586 |
587 | credentials = property(get_credentials, set_credentials)
588 |
589 | def set_flow(self, flow):
590 | self._tls.flow = flow
591 |
592 | def get_flow(self):
593 | """A thread local Flow object.
594 |
595 | Returns:
596 | A credentials.Flow object, or None if the flow hasn't been set in this
597 | thread yet, which happens in _create_flow() since Flows are created
598 | lazily.
599 | """
600 | return getattr(self._tls, 'flow', None)
601 |
602 | flow = property(get_flow, set_flow)
603 |
604 |
605 | @util.positional(4)
606 | def __init__(self, client_id, client_secret, scope,
607 | auth_uri=GOOGLE_AUTH_URI,
608 | token_uri=GOOGLE_TOKEN_URI,
609 | revoke_uri=GOOGLE_REVOKE_URI,
610 | user_agent=None,
611 | message=None,
612 | callback_path='/oauth2callback',
613 | token_response_param=None,
614 | _storage_class=StorageByKeyName,
615 | _credentials_class=CredentialsModel,
616 | _credentials_property_name='credentials',
617 | **kwargs):
618 |
619 | """Constructor for OAuth2Decorator
620 |
621 | Args:
622 | client_id: string, client identifier.
623 | client_secret: string client secret.
624 | scope: string or iterable of strings, scope(s) of the credentials being
625 | requested.
626 | auth_uri: string, URI for authorization endpoint. For convenience
627 | defaults to Google's endpoints but any OAuth 2.0 provider can be used.
628 | token_uri: string, URI for token endpoint. For convenience
629 | defaults to Google's endpoints but any OAuth 2.0 provider can be used.
630 | revoke_uri: string, URI for revoke endpoint. For convenience
631 | defaults to Google's endpoints but any OAuth 2.0 provider can be used.
632 | user_agent: string, User agent of your application, default to None.
633 | message: Message to display if there are problems with the OAuth 2.0
634 | configuration. The message may contain HTML and will be presented on the
635 | web interface for any method that uses the decorator.
636 | callback_path: string, The absolute path to use as the callback URI. Note
637 | that this must match up with the URI given when registering the
638 | application in the APIs Console.
639 | token_response_param: string. If provided, the full JSON response
640 | to the access token request will be encoded and included in this query
641 | parameter in the callback URI. This is useful with providers (e.g.
642 | wordpress.com) that include extra fields that the client may want.
643 | _storage_class: "Protected" keyword argument not typically provided to
644 | this constructor. A storage class to aid in storing a Credentials object
645 | for a user in the datastore. Defaults to StorageByKeyName.
646 | _credentials_class: "Protected" keyword argument not typically provided to
647 | this constructor. A db or ndb Model class to hold credentials. Defaults
648 | to CredentialsModel.
649 | _credentials_property_name: "Protected" keyword argument not typically
650 | provided to this constructor. A string indicating the name of the field
651 | on the _credentials_class where a Credentials object will be stored.
652 | Defaults to 'credentials'.
653 | **kwargs: dict, Keyword arguments are be passed along as kwargs to the
654 | OAuth2WebServerFlow constructor.
655 | """
656 | self._tls = threading.local()
657 | self.flow = None
658 | self.credentials = None
659 | self._client_id = client_id
660 | self._client_secret = client_secret
661 | self._scope = util.scopes_to_string(scope)
662 | self._auth_uri = auth_uri
663 | self._token_uri = token_uri
664 | self._revoke_uri = revoke_uri
665 | self._user_agent = user_agent
666 | self._kwargs = kwargs
667 | self._message = message
668 | self._in_error = False
669 | self._callback_path = callback_path
670 | self._token_response_param = token_response_param
671 | self._storage_class = _storage_class
672 | self._credentials_class = _credentials_class
673 | self._credentials_property_name = _credentials_property_name
674 |
675 | def _display_error_message(self, request_handler):
676 | request_handler.response.out.write('')
677 | request_handler.response.out.write(_safe_html(self._message))
678 | request_handler.response.out.write('')
679 |
680 | def oauth_required(self, method):
681 | """Decorator that starts the OAuth 2.0 dance.
682 |
683 | Starts the OAuth dance for the logged in user if they haven't already
684 | granted access for this application.
685 |
686 | Args:
687 | method: callable, to be decorated method of a webapp.RequestHandler
688 | instance.
689 | """
690 |
691 | def check_oauth(request_handler, *args, **kwargs):
692 | if self._in_error:
693 | self._display_error_message(request_handler)
694 | return
695 |
696 | user = users.get_current_user()
697 | # Don't use @login_decorator as this could be used in a POST request.
698 | if not user:
699 | request_handler.redirect(users.create_login_url(
700 | request_handler.request.uri))
701 | return
702 |
703 | self._create_flow(request_handler)
704 |
705 | # Store the request URI in 'state' so we can use it later
706 | self.flow.params['state'] = _build_state_value(request_handler, user)
707 | self.credentials = self._storage_class(
708 | self._credentials_class, None,
709 | self._credentials_property_name, user=user).get()
710 |
711 | if not self.has_credentials():
712 | return request_handler.redirect(self.authorize_url())
713 | try:
714 | resp = method(request_handler, *args, **kwargs)
715 | except AccessTokenRefreshError:
716 | return request_handler.redirect(self.authorize_url())
717 | finally:
718 | self.credentials = None
719 | return resp
720 |
721 | return check_oauth
722 |
723 | def _create_flow(self, request_handler):
724 | """Create the Flow object.
725 |
726 | The Flow is calculated lazily since we don't know where this app is
727 | running until it receives a request, at which point redirect_uri can be
728 | calculated and then the Flow object can be constructed.
729 |
730 | Args:
731 | request_handler: webapp.RequestHandler, the request handler.
732 | """
733 | if self.flow is None:
734 | redirect_uri = request_handler.request.relative_url(
735 | self._callback_path) # Usually /oauth2callback
736 | self.flow = OAuth2WebServerFlow(self._client_id, self._client_secret,
737 | self._scope, redirect_uri=redirect_uri,
738 | user_agent=self._user_agent,
739 | auth_uri=self._auth_uri,
740 | token_uri=self._token_uri,
741 | revoke_uri=self._revoke_uri,
742 | **self._kwargs)
743 |
744 | def oauth_aware(self, method):
745 | """Decorator that sets up for OAuth 2.0 dance, but doesn't do it.
746 |
747 | Does all the setup for the OAuth dance, but doesn't initiate it.
748 | This decorator is useful if you want to create a page that knows
749 | whether or not the user has granted access to this application.
750 | From within a method decorated with @oauth_aware the has_credentials()
751 | and authorize_url() methods can be called.
752 |
753 | Args:
754 | method: callable, to be decorated method of a webapp.RequestHandler
755 | instance.
756 | """
757 |
758 | def setup_oauth(request_handler, *args, **kwargs):
759 | if self._in_error:
760 | self._display_error_message(request_handler)
761 | return
762 |
763 | user = users.get_current_user()
764 | # Don't use @login_decorator as this could be used in a POST request.
765 | if not user:
766 | request_handler.redirect(users.create_login_url(
767 | request_handler.request.uri))
768 | return
769 |
770 | self._create_flow(request_handler)
771 |
772 | self.flow.params['state'] = _build_state_value(request_handler, user)
773 | self.credentials = self._storage_class(
774 | self._credentials_class, None,
775 | self._credentials_property_name, user=user).get()
776 | try:
777 | resp = method(request_handler, *args, **kwargs)
778 | finally:
779 | self.credentials = None
780 | return resp
781 | return setup_oauth
782 |
783 |
784 | def has_credentials(self):
785 | """True if for the logged in user there are valid access Credentials.
786 |
787 | Must only be called from with a webapp.RequestHandler subclassed method
788 | that had been decorated with either @oauth_required or @oauth_aware.
789 | """
790 | return self.credentials is not None and not self.credentials.invalid
791 |
792 | def authorize_url(self):
793 | """Returns the URL to start the OAuth dance.
794 |
795 | Must only be called from with a webapp.RequestHandler subclassed method
796 | that had been decorated with either @oauth_required or @oauth_aware.
797 | """
798 | url = self.flow.step1_get_authorize_url()
799 | return str(url)
800 |
801 | def http(self):
802 | """Returns an authorized http instance.
803 |
804 | Must only be called from within an @oauth_required decorated method, or
805 | from within an @oauth_aware decorated method where has_credentials()
806 | returns True.
807 | """
808 | return self.credentials.authorize(httplib2.Http())
809 |
810 | @property
811 | def callback_path(self):
812 | """The absolute path where the callback will occur.
813 |
814 | Note this is the absolute path, not the absolute URI, that will be
815 | calculated by the decorator at runtime. See callback_handler() for how this
816 | should be used.
817 |
818 | Returns:
819 | The callback path as a string.
820 | """
821 | return self._callback_path
822 |
823 |
824 | def callback_handler(self):
825 | """RequestHandler for the OAuth 2.0 redirect callback.
826 |
827 | Usage:
828 | app = webapp.WSGIApplication([
829 | ('/index', MyIndexHandler),
830 | ...,
831 | (decorator.callback_path, decorator.callback_handler())
832 | ])
833 |
834 | Returns:
835 | A webapp.RequestHandler that handles the redirect back from the
836 | server during the OAuth 2.0 dance.
837 | """
838 | decorator = self
839 |
840 | class OAuth2Handler(webapp.RequestHandler):
841 | """Handler for the redirect_uri of the OAuth 2.0 dance."""
842 |
843 | @login_required
844 | def get(self):
845 | error = self.request.get('error')
846 | if error:
847 | errormsg = self.request.get('error_description', error)
848 | self.response.out.write(
849 | 'The authorization request failed: %s' % _safe_html(errormsg))
850 | else:
851 | user = users.get_current_user()
852 | decorator._create_flow(self)
853 | credentials = decorator.flow.step2_exchange(self.request.params)
854 | decorator._storage_class(
855 | decorator._credentials_class, None,
856 | decorator._credentials_property_name, user=user).put(credentials)
857 | redirect_uri = _parse_state_value(str(self.request.get('state')),
858 | user)
859 |
860 | if decorator._token_response_param and credentials.token_response:
861 | resp_json = simplejson.dumps(credentials.token_response)
862 | redirect_uri = util._add_query_parameter(
863 | redirect_uri, decorator._token_response_param, resp_json)
864 |
865 | self.redirect(redirect_uri)
866 |
867 | return OAuth2Handler
868 |
869 | def callback_application(self):
870 | """WSGI application for handling the OAuth 2.0 redirect callback.
871 |
872 | If you need finer grained control use `callback_handler` which returns just
873 | the webapp.RequestHandler.
874 |
875 | Returns:
876 | A webapp.WSGIApplication that handles the redirect back from the
877 | server during the OAuth 2.0 dance.
878 | """
879 | return webapp.WSGIApplication([
880 | (self.callback_path, self.callback_handler())
881 | ])
882 |
883 |
884 | class OAuth2DecoratorFromClientSecrets(OAuth2Decorator):
885 | """An OAuth2Decorator that builds from a clientsecrets file.
886 |
887 | Uses a clientsecrets file as the source for all the information when
888 | constructing an OAuth2Decorator.
889 |
890 | Example:
891 |
892 | decorator = OAuth2DecoratorFromClientSecrets(
893 | os.path.join(os.path.dirname(__file__), 'client_secrets.json')
894 | scope='https://www.googleapis.com/auth/plus')
895 |
896 |
897 | class MainHandler(webapp.RequestHandler):
898 |
899 | @decorator.oauth_required
900 | def get(self):
901 | http = decorator.http()
902 | # http is authorized with the user's Credentials and can be used
903 | # in API calls
904 | """
905 |
906 | @util.positional(3)
907 | def __init__(self, filename, scope, message=None, cache=None):
908 | """Constructor
909 |
910 | Args:
911 | filename: string, File name of client secrets.
912 | scope: string or iterable of strings, scope(s) of the credentials being
913 | requested.
914 | message: string, A friendly string to display to the user if the
915 | clientsecrets file is missing or invalid. The message may contain HTML
916 | and will be presented on the web interface for any method that uses the
917 | decorator.
918 | cache: An optional cache service client that implements get() and set()
919 | methods. See clientsecrets.loadfile() for details.
920 | """
921 | client_type, client_info = clientsecrets.loadfile(filename, cache=cache)
922 | if client_type not in [
923 | clientsecrets.TYPE_WEB, clientsecrets.TYPE_INSTALLED]:
924 | raise InvalidClientSecretsError(
925 | 'OAuth2Decorator doesn\'t support this OAuth 2.0 flow.')
926 | constructor_kwargs = {
927 | 'auth_uri': client_info['auth_uri'],
928 | 'token_uri': client_info['token_uri'],
929 | 'message': message,
930 | }
931 | revoke_uri = client_info.get('revoke_uri')
932 | if revoke_uri is not None:
933 | constructor_kwargs['revoke_uri'] = revoke_uri
934 | super(OAuth2DecoratorFromClientSecrets, self).__init__(
935 | client_info['client_id'], client_info['client_secret'],
936 | scope, **constructor_kwargs)
937 | if message is not None:
938 | self._message = message
939 | else:
940 | self._message = 'Please configure your application for OAuth 2.0.'
941 |
942 |
943 | @util.positional(2)
944 | def oauth2decorator_from_clientsecrets(filename, scope,
945 | message=None, cache=None):
946 | """Creates an OAuth2Decorator populated from a clientsecrets file.
947 |
948 | Args:
949 | filename: string, File name of client secrets.
950 | scope: string or list of strings, scope(s) of the credentials being
951 | requested.
952 | message: string, A friendly string to display to the user if the
953 | clientsecrets file is missing or invalid. The message may contain HTML and
954 | will be presented on the web interface for any method that uses the
955 | decorator.
956 | cache: An optional cache service client that implements get() and set()
957 | methods. See clientsecrets.loadfile() for details.
958 |
959 | Returns: An OAuth2Decorator
960 |
961 | """
962 | return OAuth2DecoratorFromClientSecrets(filename, scope,
963 | message=message, cache=cache)
964 |
--------------------------------------------------------------------------------