Pārlūkot izejas kodu

HUE-966 [desktop] OAuthBackend and view for Oauth

Based on python-oauth2 and the example works with Twitter.

Requires: python-oauth2 and httplib2
build/env/bin/python setup.py install https://github.com/simplegeo/python-oauth2
build/env/bin/pip install httplib2

Need to switch the auth backend to OAuthBackend.
Romain Rigaux 12 gadi atpakaļ
vecāks
revīzija
8255a8a
31 mainītis faili ar 3974 papildinājumiem un 13 dzēšanām
  1. 20 0
      desktop/conf.dist/hue.ini
  2. 20 0
      desktop/conf/pseudo-distributed.ini.tmpl
  3. 3 0
      desktop/core/ext-py/python-oauth2/.gitignore
  4. 21 0
      desktop/core/ext-py/python-oauth2/LICENSE.txt
  5. 155 0
      desktop/core/ext-py/python-oauth2/Makefile
  6. 357 0
      desktop/core/ext-py/python-oauth2/README.md
  7. 408 0
      desktop/core/ext-py/python-oauth2/debian/changelog
  8. 1 0
      desktop/core/ext-py/python-oauth2/debian/compat
  9. 24 0
      desktop/core/ext-py/python-oauth2/debian/control
  10. 21 0
      desktop/core/ext-py/python-oauth2/debian/copyright
  11. 1 0
      desktop/core/ext-py/python-oauth2/debian/pycompat
  12. 1 0
      desktop/core/ext-py/python-oauth2/debian/pyversions
  13. 12 0
      desktop/core/ext-py/python-oauth2/debian/rules
  14. 108 0
      desktop/core/ext-py/python-oauth2/example/appengine_oauth.py
  15. 165 0
      desktop/core/ext-py/python-oauth2/example/client.py
  16. 195 0
      desktop/core/ext-py/python-oauth2/example/server.py
  17. 860 0
      desktop/core/ext-py/python-oauth2/oauth2/__init__.py
  18. 18 0
      desktop/core/ext-py/python-oauth2/oauth2/_version.py
  19. 0 0
      desktop/core/ext-py/python-oauth2/oauth2/clients/__init__.py
  20. 40 0
      desktop/core/ext-py/python-oauth2/oauth2/clients/imap.py
  21. 41 0
      desktop/core/ext-py/python-oauth2/oauth2/clients/smtp.py
  22. 0 0
      desktop/core/ext-py/python-oauth2/setup.cfg
  23. 40 0
      desktop/core/ext-py/python-oauth2/setup.py
  24. 0 0
      desktop/core/ext-py/python-oauth2/tests/__init__.py
  25. 1309 0
      desktop/core/ext-py/python-oauth2/tests/test_oauth.py
  26. 29 0
      desktop/core/src/desktop/auth/backend.py
  27. 56 7
      desktop/core/src/desktop/auth/views.py
  28. 43 2
      desktop/core/src/desktop/conf.py
  29. 1 0
      desktop/core/src/desktop/lib/rest/http_client.py
  30. 21 4
      desktop/core/src/desktop/templates/login.mako
  31. 4 0
      desktop/core/src/desktop/urls.py

+ 20 - 0
desktop/conf.dist/hue.ini

@@ -91,6 +91,7 @@
     # - desktop.auth.backend.PamBackend
     # - desktop.auth.backend.SpnegoDjangoBackend
     # - desktop.auth.backend.RemoteUserDjangoBackend
+    # - desktop.auth.backend.OAuthBackend
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
     ## pam_service=login
@@ -206,6 +207,25 @@
     ## kinit_path=/path/to/kinit
 
 
+  # Configuration options for using OAuthBackend login
+  # ------------------------------------------------------------------------
+  [[oauth]]
+    # The Consumer key of the application
+    ## consumer_key=XXXXXXXXXXXXXXXXXXXXX
+
+    # The Consumer secret of the application
+    ## consumer_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+    # The Request token URL
+    ## request_token_url=https://api.twitter.com/oauth/request_token
+
+    # The Access token URL
+    ## access_token_url=https://api.twitter.com/oauth/access_token
+
+    # The Authorize URL
+    ## authenticate_url=https://api.twitter.com/oauth/authorize
+
+
 ###########################################################################
 # Settings to configure your Hadoop cluster.
 ###########################################################################

+ 20 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -100,6 +100,7 @@
     # - desktop.auth.backend.PamBackend
     # - desktop.auth.backend.SpnegoDjangoBackend
     # - desktop.auth.backend.RemoteUserDjangoBackend
+    # - desktop.auth.backend.OAuthBackend
     ## backend=desktop.auth.backend.AllowFirstUserDjangoBackend
 
     ## pam_service=login
@@ -212,6 +213,25 @@
     ## kinit_path=/path/to/kinit
 
 
+  # Configuration options for using OAuthBackend login
+  # ------------------------------------------------------------------------
+  [[oauth]]
+    # The Consumer key of the application
+    ## consumer_key=XXXXXXXXXXXXXXXXXXXXX
+
+    # The Consumer secret of the application
+    ## consumer_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+    # The Request token URL
+    ## request_token_url=https://api.twitter.com/oauth/request_token
+
+    # The Access token URL
+    ## access_token_url=https://api.twitter.com/oauth/access_token
+
+    # The Authorize URL
+    ## authenticate_url=https://api.twitter.com/oauth/authorize
+
+
 ###########################################################################
 # Settings to configure your Hadoop cluster.
 ###########################################################################

+ 3 - 0
desktop/core/ext-py/python-oauth2/.gitignore

@@ -0,0 +1,3 @@
+*.py?
+*.egg-info
+*.swp

+ 21 - 0
desktop/core/ext-py/python-oauth2/LICENSE.txt

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 155 - 0
desktop/core/ext-py/python-oauth2/Makefile

@@ -0,0 +1,155 @@
+PYTHON      = $(shell test -x bin/python && echo bin/python || \
+                      echo `which python`)
+PYVERS      = $(shell $(PYTHON) -c 'import sys; print "%s.%s" % sys.version_info[0:2]')
+VIRTUALENV  = $(shell /bin/echo -n `which virtualenv || \
+                                    which virtualenv-$(PYVERS) || \
+                                    which virtualenv$(PYVERS)`)
+VIRTUALENV += --no-site-packages
+PAGER      ?= less
+DEPS       := $(shell find $(PWD)/deps -type f -printf "file://%p ")
+COVERAGE    = $(shell test -x bin/coverage && echo bin/coverage || echo true)
+SETUP       = $(PYTHON) ./setup.py
+EZ_INSTALL  = $(SETUP) easy_install -f "$(DEPS)"
+PYLINT      = bin/pylint
+PLATFORM    = $(shell $(PYTHON) -c "from pkg_resources import get_build_platform; print get_build_platform()")
+OS         := $(shell uname)
+EGG        := $(shell $(SETUP) --fullname)-py$(PYVERS).egg
+SDIST      := $(shell $(SETUP) --fullname).tar.gs
+SRCDIR     := oauth2
+SOURCES    := $(shell find $(SRCDIR) -type f -name \*.py -not -name 'test_*')
+TESTS      := $(shell find $(SRCDIR) -type f -name test_\*.py)
+COVERED    := $(SOURCES)
+ROOT        = $(shell pwd)
+ROOTCMD     = fakeroot
+SIGN_KEY   ?= nerds@simplegeo.com
+BUILD_NUMBER ?= 1
+
+
+.PHONY: test dev clean extraclean debian/changelog
+
+all: egg
+egg: dist/$(EGG)
+
+dist/$(EGG):
+	$(SETUP) bdist_egg
+
+sdist:
+	$(SETUP) sdist
+
+debian/changelog:
+	-git branch -D changelog
+	git checkout -b changelog
+	git-dch -a -N $(shell $(SETUP) --version) --debian-branch changelog \
+            --snapshot --snapshot-number=$(BUILD_NUMBER)
+
+deb: debian/changelog
+	test -d dist/deb || mkdir -p dist/deb
+	dpkg-buildpackage -r$(ROOTCMD) -k$(SIGN_KEY)
+	mv ../python-oauth2_* dist/deb
+
+test:
+	$(SETUP) test --with-coverage --cover-package=oauth2
+
+sdist:
+	python setup.py sdist
+
+xunit.xml: bin/nosetests $(SOURCES) $(TESTS)
+	$(SETUP) test --with-xunit --xunit-file=$@
+
+bin/nosetests: bin/easy_install
+	@$(EZ_INSTALL) nose
+
+coverage: .coverage
+	@$(COVERAGE) html -d $@ $(COVERED)
+
+coverage.xml: .coverage
+	@$(COVERAGE) xml $(COVERED)
+
+.coverage: $(SOURCES) $(TESTS) bin/coverage bin/nosetests
+	-@$(COVERAGE) run $(SETUP) test
+
+bin/coverage: bin/easy_install
+	@$(EZ_INSTALL) coverage
+
+profile: .profile bin/pyprof2html
+	bin/pyprof2html -o $@ $<
+
+.profile: $(SOURCES) bin/nosetests
+	-$(SETUP) test -q --with-profile --profile-stats-file=$@
+
+bin/pyprof2html: bin/easy_install bin/
+	@$(EZ_INSTALL) pyprof2html
+
+docs: $(SOURCES) bin/epydoc
+	@echo bin/epydoc -q --html --no-frames -o $@ ...
+	@bin/epydoc -q --html --no-frames -o $@ $(SOURCES)
+
+bin/epydoc: bin/easy_install
+	@$(EZ_INSTALL) epydoc
+
+bin/pep8: bin/easy_install
+	@$(EZ_INSTALL) pep8
+
+pep8: bin/pep8
+	@bin/pep8 --repeat --ignore E225 $(SRCDIR)
+
+pep8.txt: bin/pep8
+	@bin/pep8 --repeat --ignore E225 $(SRCDIR) > $@
+
+lint: bin/pylint
+	-$(PYLINT) -f colorized $(SRCDIR)
+
+lint.html: bin/pylint
+	-$(PYLINT) -f html $(SRCDIR) > $@
+
+lint.txt: bin/pylint
+	-$(PYLINT) -f parseable $(SRCDIR) > $@
+
+bin/pylint: bin/easy_install
+	@$(EZ_INSTALL) pylint
+
+README.html: README.mkd | bin/markdown
+	bin/markdown -e utf-8 $^ -f $@
+
+bin/markdown: bin/easy_install
+	@$(EZ_INSTALL) Markdown
+
+
+# Development setup
+rtfm:
+	$(PAGER) README.mkd
+
+tags: TAGS.gz
+
+TAGS.gz: TAGS
+	gzip $^
+
+TAGS: $(SOURCES)
+	ctags -eR .
+
+env: bin/easy_install
+
+bin/easy_install:
+	$(VIRTUALENV) .
+	-test -f deps/setuptools* && $@ -U deps/setuptools*
+
+dev: develop
+develop: env
+	nice -n 20 $(SETUP) develop
+	@echo "            ---------------------------------------------"
+	@echo "            To activate the development environment, run:"
+	@echo "                           . bin/activate"
+	@echo "            ---------------------------------------------"
+
+clean:
+clean:
+	find . -type f -name \*.pyc -exec rm {} \;
+	rm -rf build dist TAGS TAGS.gz digg.egg-info tmp .coverage \
+	       coverage coverage.xml docs lint.html lint.txt profile \
+	       .profile *.egg xunit.xml
+	@if test "$(OS)" = "Linux"; then $(ROOTCMD) debian/rules clean; fi
+
+
+xclean: extraclean
+extraclean: clean
+	rm -rf bin lib .Python include

+ 357 - 0
desktop/core/ext-py/python-oauth2/README.md

@@ -0,0 +1,357 @@
+# Overview
+
+This code was originally forked from [Leah Culver and Andy Smith's oauth.py code](http://github.com/leah/python-oauth/). Some of the tests come from a [fork by Vic Fryzel](http://github.com/shellsage/python-oauth), while a revamped Request class and more tests were merged in from [Mark Paschal's fork](http://github.com/markpasc/python-oauth). A number of notable differences exist between this code and its forefathers:
+
+* 100% unit test coverage.
+* The <code>DataStore</code> object has been completely ripped out. While creating unit tests for the library I found several substantial bugs with the implementation and confirmed with Andy Smith that it was never fully baked.
+* Classes are no longer prefixed with <code>OAuth</code>.
+* The <code>Request</code> class now extends from <code>dict</code>.
+* The library is likely no longer compatible with Python 2.3.
+* The <code>Client</code> class works and extends from <code>httplib2</code>. It's a thin wrapper that handles automatically signing any normal HTTP request you might wish to make.
+
+# Signing a Request
+
+    import oauth2 as oauth
+    import time
+    
+    # Set the API endpoint 
+    url = "http://example.com/photos"
+    
+    # Set the base oauth_* parameters along with any other parameters required
+    # for the API call.
+    params = {
+        'oauth_version': "1.0",
+        'oauth_nonce': oauth.generate_nonce(),
+        'oauth_timestamp': int(time.time())
+        'user': 'joestump',
+        'photoid': 555555555555
+    }
+    
+    # Set up instances of our Token and Consumer. The Consumer.key and 
+    # Consumer.secret are given to you by the API provider. The Token.key and
+    # Token.secret is given to you after a three-legged authentication.
+    token = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+    consumer = oauth.Consumer(key="con-test-key", secret="con-test-secret")
+    
+    # Set our token/key parameters
+    params['oauth_token'] = token.key
+    params['oauth_consumer_key'] = consumer.key
+    
+    # Create our request. Change method, etc. accordingly.
+    req = oauth.Request(method="GET", url=url, parameters=params)
+    
+    # Sign the request.
+    signature_method = oauth.SignatureMethod_HMAC_SHA1()
+    req.sign_request(signature_method, consumer, token)
+
+# Using the Client
+
+The <code>oauth2.Client</code> is based on <code>httplib2</code> and works just as you'd expect it to. The only difference is the first two arguments to the constructor are an instance of <code>oauth2.Consumer</code> and <code>oauth2.Token</code> (<code>oauth2.Token</code> is only needed for three-legged requests).
+
+    import oauth2 as oauth
+    
+    # Create your consumer with the proper key/secret.
+    consumer = oauth.Consumer(key="your-twitter-consumer-key", 
+        secret="your-twitter-consumer-secret")
+    
+    # Request token URL for Twitter.
+    request_token_url = "http://twitter.com/oauth/request_token"
+    
+    # Create our client.
+    client = oauth.Client(consumer)
+    
+    # The OAuth Client request works just like httplib2 for the most part.
+    resp, content = client.request(request_token_url, "GET")
+    print resp
+    print content
+
+# Twitter Three-legged OAuth Example
+
+Below is an example of how one would go through a three-legged OAuth flow to
+gain access to protected resources on Twitter. This is a simple CLI script, but
+can be easily translated to a web application.
+
+    import urlparse
+    import oauth2 as oauth
+    
+    consumer_key = 'my_key_from_twitter'
+    consumer_secret = 'my_secret_from_twitter'
+    
+    request_token_url = 'http://twitter.com/oauth/request_token'
+    access_token_url = 'http://twitter.com/oauth/access_token'
+    authorize_url = 'http://twitter.com/oauth/authorize'
+    
+    consumer = oauth.Consumer(consumer_key, consumer_secret)
+    client = oauth.Client(consumer)
+    
+    # Step 1: Get a request token. This is a temporary token that is used for 
+    # having the user authorize an access token and to sign the request to obtain 
+    # said access token.
+    
+    resp, content = client.request(request_token_url, "GET")
+    if resp['status'] != '200':
+        raise Exception("Invalid response %s." % resp['status'])
+    
+    request_token = dict(urlparse.parse_qsl(content))
+    
+    print "Request Token:"
+    print "    - oauth_token        = %s" % request_token['oauth_token']
+    print "    - oauth_token_secret = %s" % request_token['oauth_token_secret']
+    print 
+    
+    # Step 2: Redirect to the provider. Since this is a CLI script we do not 
+    # redirect. In a web application you would redirect the user to the URL
+    # below.
+    
+    print "Go to the following link in your browser:"
+    print "%s?oauth_token=%s" % (authorize_url, request_token['oauth_token'])
+    print 
+    
+    # After the user has granted access to you, the consumer, the provider will
+    # redirect you to whatever URL you have told them to redirect to. You can 
+    # usually define this in the oauth_callback argument as well.
+    accepted = 'n'
+    while accepted.lower() == 'n':
+        accepted = raw_input('Have you authorized me? (y/n) ')
+    oauth_verifier = raw_input('What is the PIN? ')
+    
+    # Step 3: Once the consumer has redirected the user back to the oauth_callback
+    # URL you can request the access token the user has approved. You use the 
+    # request token to sign this request. After this is done you throw away the
+    # request token and use the access token returned. You should store this 
+    # access token somewhere safe, like a database, for future use.
+    token = oauth.Token(request_token['oauth_token'],
+        request_token['oauth_token_secret'])
+    token.set_verifier(oauth_verifier)
+    client = oauth.Client(consumer, token)
+    
+    resp, content = client.request(access_token_url, "POST")
+    access_token = dict(urlparse.parse_qsl(content))
+    
+    print "Access Token:"
+    print "    - oauth_token        = %s" % access_token['oauth_token']
+    print "    - oauth_token_secret = %s" % access_token['oauth_token_secret']
+    print
+    print "You may now access protected resources using the access tokens above." 
+    print
+
+# Logging into Django w/ Twitter
+
+Twitter also has the ability to authenticate a user [via an OAuth flow](http://apiwiki.twitter.com/Sign-in-with-Twitter). This
+flow is exactly like the three-legged OAuth flow, except you send them to a 
+slightly different URL to authorize them. 
+
+In this example we'll look at how you can implement this login flow using 
+Django and python-oauth2. 
+
+## Set up a Profile model
+
+You'll need a place to store all of your Twitter OAuth credentials after the
+user has logged in. In your app's `models.py` file you should add something
+that resembles the following model.
+
+    class Profile(models.Model):
+        user = models.ForeignKey(User)
+        oauth_token = models.CharField(max_length=200)
+        oauth_secret = models.CharField(max_length=200)
+
+## Set up your Django views
+
+### `urls.py`
+
+Your `urls.py` should look something like the following. Basically, you need to
+have a login URL, a callback URL that Twitter will redirect your users back to,
+and a logout URL.
+
+In this example `^login/` and `twitter_login` will send the user to Twitter to
+be logged in, `^login/authenticated/` and `twitter_authenticated` will confirm
+the login, create the account if necessary, and log the user into the 
+application, and `^logout`/ logs the user out in the `twitter_logout` view.
+
+
+    from django.conf.urls.defaults import *
+    from django.contrib import admin
+    from mytwitterapp.views import twitter_login, twitter_logout, \
+        twitter_authenticated
+
+    admin.autodiscover()
+
+    urlpatterns = patterns('',
+        url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+        url(r'^admin/', include(admin.site.urls)),
+        url(r'^login/?$', twitter_login),
+        url(r'^logout/?$', twitter_logout),
+        url(r'^login/authenticated/?$', twitter_authenticated),
+    )
+
+### `views.py`
+
+*NOTE:* The following code was coded for Python 2.4 so some of the libraries 
+and code here might need to be updated if you are using Python 2.6+. 
+
+    # Python
+    import oauth2 as oauth
+    import cgi
+
+    # Django
+    from django.shortcuts import render_to_response
+    from django.http import HttpResponseRedirect
+    from django.conf import settings
+    from django.contrib.auth import authenticate, login, logout
+    from django.contrib.auth.models import User
+    from django.contrib.auth.decorators import login_required
+
+    # Project
+    from mytwitterapp.models import Profile
+
+    # It's probably a good idea to put your consumer's OAuth token and
+    # OAuth secret into your project's settings. 
+    consumer = oauth.Consumer(settings.TWITTER_TOKEN, settings.TWITTER_SECRET)
+    client = oauth.Client(consumer)
+
+    request_token_url = 'http://twitter.com/oauth/request_token'
+    access_token_url = 'http://twitter.com/oauth/access_token'
+
+    # This is the slightly different URL used to authenticate/authorize.
+    authenticate_url = 'http://twitter.com/oauth/authenticate'
+
+    def twitter_login(request):
+        # Step 1. Get a request token from Twitter.
+        resp, content = client.request(request_token_url, "GET")
+        if resp['status'] != '200':
+            raise Exception("Invalid response from Twitter.")
+
+        # Step 2. Store the request token in a session for later use.
+        request.session['request_token'] = dict(cgi.parse_qsl(content))
+
+        # Step 3. Redirect the user to the authentication URL.
+        url = "%s?oauth_token=%s" % (authenticate_url,
+            request.session['request_token']['oauth_token'])
+
+        return HttpResponseRedirect(url)
+
+    
+    @login_required
+    def twitter_logout(request):
+        # Log a user out using Django's logout function and redirect them
+        # back to the homepage.
+        logout(request)
+        return HttpResponseRedirect('/')
+
+    def twitter_authenticated(request):
+        # Step 1. Use the request token in the session to build a new client.
+        token = oauth.Token(request.session['request_token']['oauth_token'],
+            request.session['request_token']['oauth_token_secret'])
+        client = oauth.Client(consumer, token)
+    
+        # Step 2. Request the authorized access token from Twitter.
+        resp, content = client.request(access_token_url, "GET")
+        if resp['status'] != '200':
+            print content
+            raise Exception("Invalid response from Twitter.")
+    
+        """
+        This is what you'll get back from Twitter. Note that it includes the
+        user's user_id and screen_name.
+        {
+            'oauth_token_secret': 'IcJXPiJh8be3BjDWW50uCY31chyhsMHEhqJVsphC3M',
+            'user_id': '120889797', 
+            'oauth_token': '120889797-H5zNnM3qE0iFoTTpNEHIz3noL9FKzXiOxwtnyVOD',
+            'screen_name': 'heyismysiteup'
+        }
+        """
+        access_token = dict(cgi.parse_qsl(content))
+    
+        # Step 3. Lookup the user or create them if they don't exist.
+        try:
+            user = User.objects.get(username=access_token['screen_name'])
+        except User.DoesNotExist:
+            # When creating the user I just use their screen_name@twitter.com
+            # for their email and the oauth_token_secret for their password.
+            # These two things will likely never be used. Alternatively, you 
+            # can prompt them for their email here. Either way, the password 
+            # should never be used.
+            user = User.objects.create_user(access_token['screen_name'],
+                '%s@twitter.com' % access_token['screen_name'],
+                access_token['oauth_token_secret'])
+    
+            # Save our permanent token and secret for later.
+            profile = Profile()
+            profile.user = user
+            profile.oauth_token = access_token['oauth_token']
+            profile.oauth_secret = access_token['oauth_token_secret']
+            profile.save()
+    
+        # Authenticate the user and log them in using Django's pre-built 
+        # functions for these things.
+        user = authenticate(username=access_token['screen_name'],
+            password=access_token['oauth_token_secret'])
+        login(request, user)
+    
+        return HttpResponseRedirect('/')
+    
+
+### `settings.py`
+
+* You'll likely want to set `LOGIN_URL` to `/login/` so that users are properly redirected to your Twitter login handler when you use `@login_required` in other parts of your Django app.
+* You can also set `AUTH_PROFILE_MODULE = 'mytwitterapp.Profile'` so that you can easily access the Twitter OAuth token/secret for that user using the `User.get_profile()` method in Django.
+
+# XOAUTH for IMAP and SMTP
+
+Gmail supports OAuth over IMAP and SMTP via a standard they call XOAUTH. This allows you to authenticate against Gmail's IMAP and SMTP servers using an OAuth token and secret. It also has the added benefit of allowing you to use vanilla SMTP and IMAP libraries. The `python-oauth2` package provides both IMAP and SMTP libraries that implement XOAUTH and wrap `imaplib.IMAP4_SSL` and `smtplib.SMTP`. This allows you to connect to Gmail with OAuth credentials using standard Python libraries. 
+
+## IMAP
+
+    import oauth2 as oauth
+    import oauth2.clients.imap as imaplib
+
+    # Set up your Consumer and Token as per usual. Just like any other
+    # three-legged OAuth request.
+    consumer = oauth.Consumer('your_consumer_key', 'your_consumer_secret')
+    token = oauth.Token('your_users_3_legged_token', 
+        'your_users_3_legged_token_secret')
+
+    # Setup the URL according to Google's XOAUTH implementation. Be sure
+    # to replace the email here with the appropriate email address that
+    # you wish to access.
+    url = "https://mail.google.com/mail/b/your_users_email@gmail.com/imap/"
+
+    conn = imaplib.IMAP4_SSL('imap.googlemail.com')
+    conn.debug = 4 
+
+    # This is the only thing in the API for impaplib.IMAP4_SSL that has 
+    # changed. You now authenticate with the URL, consumer, and token.
+    conn.authenticate(url, consumer, token)
+
+    # Once authenticated everything from the impalib.IMAP4_SSL class will 
+    # work as per usual without any modification to your code.
+    conn.select('INBOX')
+    print conn.list()
+
+
+## SMTP
+
+    import oauth2 as oauth
+    import oauth2.clients.smtp as smtplib
+
+    # Set up your Consumer and Token as per usual. Just like any other
+    # three-legged OAuth request.
+    consumer = oauth.Consumer('your_consumer_key', 'your_consumer_secret')
+    token = oauth.Token('your_users_3_legged_token', 
+        'your_users_3_legged_token_secret')
+
+    # Setup the URL according to Google's XOAUTH implementation. Be sure
+    # to replace the email here with the appropriate email address that
+    # you wish to access.
+    url = "https://mail.google.com/mail/b/your_users_email@gmail.com/smtp/"
+
+    conn = smtplib.SMTP('smtp.googlemail.com', 587)
+    conn.set_debuglevel(True)
+    conn.ehlo('test')
+    conn.starttls()
+
+    # Again the only thing modified from smtplib.SMTP is the authenticate
+    # method, which works identically to the imaplib.IMAP4_SSL method.
+    conn.authenticate(url, consumer, token)
+
+

+ 408 - 0
desktop/core/ext-py/python-oauth2/debian/changelog

@@ -0,0 +1,408 @@
+python-oauth2 (1.5.211) maverick; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 13 Dec 2011 17:39:07 +0000
+
+python-oauth2 (1.5.210) maverick; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 09 Dec 2011 01:17:31 +0000
+
+python-oauth2 (1.5.170) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 02 May 2011 23:48:16 +0000
+
+python-oauth2 (1.5.169) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 15 Apr 2011 21:29:41 +0000
+
+python-oauth2 (1.5.168) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 05 Apr 2011 23:13:18 +0000
+
+python-oauth2 (1.5.167) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Thu, 17 Feb 2011 21:56:49 +0000
+
+python-oauth2 (1.5.166) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * fix encoding of non-ascii data into postdata, really fixes #48 this
+    time (comes with test)
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 15 Feb 2011 17:23:00 +0000
+
+python-oauth2 (1.5.165) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * get_normalized_parameters() is required to exclude any
+    oauth_signature that was already there, and it is required to
+    preserve duplicate keys, even with identical values ref #46, fixes
+    pull/49, thanks @zyegfryed
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 07 Feb 2011 19:24:56 +0000
+
+python-oauth2 (1.5.164) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * make it actually inspect the request, and fix the test to require
+    the right things of the request ref #46, ref pull/49, thanks
+    @zyegfryed
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 07 Feb 2011 18:53:56 +0000
+
+python-oauth2 (1.5.163) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * add test of #46; fixes #46
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 04 Feb 2011 18:14:56 +0000
+
+python-oauth2 (1.5.162) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * import parse_qs from cgi if it can't be imported from urlparse, thus
+    regaining compatibility with Python 2.5 fixes #47 thanks to @vtsao,
+    @subsume, @worksology
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 04 Feb 2011 17:50:56 +0000
+
+python-oauth2 (1.5.161) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * Include all parameters from URL, even ones that begin with "oauth_",
+    in signature base. effectively reverts
+    https://github.com/simplegeo/python-
+    oauth2/commit/50ca9578f598faff5427e58814e2f45d01d3261c fixes #27
+    Thanks to @robhudson for the bug report and help debugging.
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Thu, 03 Feb 2011 18:03:59 +0000
+
+python-oauth2 (1.5.160) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * add another unicode test case to the tests just to show to user
+    saevarom on github
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Thu, 03 Feb 2011 00:01:55 +0000
+
+python-oauth2 (1.5.159) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Thu, 03 Feb 2011 00:00:04 +0000
+
+python-oauth2 (1.5.158) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * keep empty params parsed from the URL (fixes #22)
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 02 Feb 2011 19:34:25 +0000
+
+python-oauth2 (1.5.155) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * use multiple values under the same key from the body (fixes #23)
+    Thanks to rogerm, ideasculptor, jiivan, timgraham.
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 01 Feb 2011 23:43:56 +0000
+
+python-oauth2 (1.5.154) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 01 Feb 2011 18:03:58 +0000
+
+python-oauth2 (1.5.153) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * add work-around for bug unicode-handling in urlparse in Python <
+    2.7, including tests
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 01 Feb 2011 16:31:56 +0000
+
+python-oauth2 (1.5.152) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * another test for handling of non-ascii, non-utf-8 urls
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 28 Jan 2011 18:14:34 +0000
+
+python-oauth2 (1.5.151) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Thu, 27 Jan 2011 05:31:55 +0000
+
+python-oauth2 (1.5.150) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 26 Jan 2011 17:15:25 +0000
+
+python-oauth2 (1.5.149) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * add the body-hash header as specified in
+    http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-
+    bodyhash.html
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 26 Jan 2011 17:11:34 +0000
+
+python-oauth2 (1.5.148) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * default body is the empty string rather than None; add the Realm
+    header
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 26 Jan 2011 17:02:34 +0000
+
+python-oauth2 (1.5.147) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * pass the body and the is_form_encoding flag in to the Request object
+    (for future use in implementing the hash-of-body extension)
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 25 Jan 2011 17:39:34 +0000
+
+python-oauth2 (1.5.146) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * refactor internals of the way Client.request() decides whether this
+    is a x-www-form-urlencoded request or not
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 25 Jan 2011 17:30:38 +0000
+
+python-oauth2 (1.5.145) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * update version number from 1.4 to 1.5
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 22:08:36 +0000
+
+python-oauth2 (1.4.144) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * more thorough unit tests for non-ascii urls, params, and query-args-
+    in-url
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 22:02:34 +0000
+
+python-oauth2 (1.4.143) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * use two-part version number -- version is manually modified, build-
+    number is automatically modified
+
+  [ Simple Geebus ]
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 20:21:20 +0000
+
+python-oauth2 (1.4.134) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 18:30:58 +0000
+
+python-oauth2 (1.4.133) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * handle unicode objects and utf-8 strings in url and params and
+    encode them to utf-8 when serializing
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 18:25:54 +0000
+
+python-oauth2 (1.4.132) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 17:39:52 +0000
+
+python-oauth2 (1.4.131) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 17:32:18 +0000
+
+python-oauth2 (1.4.130) lucid; urgency=low
+
+  * UNRELEASED
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 24 Jan 2011 17:22:46 +0000
+
+python-oauth2 (1.4.129) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * change manually-managed version number from 1.3 to 1.4
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Fri, 21 Jan 2011 19:52:32 +0000
+
+python-oauth2 (1.3.128) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * raise an exception if the URL passed in is not a unicode object nor
+    ascii We can't submit a correct URL with arbitrary bytes -- we have
+    to submit a utf-8 encoded unicode string. (Otherwise we'll cause
+    either a rejection or a signature mismatch in the server, which is
+    what has happened at SimpleGeo.) If the caller passes in non-ascii
+    things in a str then it would be better for them to change their
+    code to decode it to unicode before passing it in than for us to
+    decode it, since they have a better chance of knowing what encoding
+    it is in -- if we did it we would be guessing.
+  * fix flaws pointed out by ieure, plus test failures I'm not entirely
+    sure it is appropriate to allow .url and .normalized_url to be non-
+    existent in addition to allowing them to be None, but I don't really
+    understand the intent of the url setter.
+  * Oh, after we've made sure it can be converted to unicode, just leave
+    it as a unicode object after that.
+  * raise more specific error message if argument to to_unicode() is
+    neither unicode nor str
+  * a couple of cleanups suggested by ieure in code review
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Mon, 10 Jan 2011 22:42:35 +0000
+
+python-oauth2 (1.3.127) lucid; urgency=low
+
+  [ Zooko Ofsimplegeo ]
+  * run tests when building
+  * raise exception from a test just to see if hudson is *really*
+    running these tests. I don't see the test results.
+  * put version number in a file where it can be read and written by
+    various things (including hudson)
+  * make oauth2.__version__ exist, rename VERSION to OAUTH_VERSION for
+    clarity
+  * debian/control -- build-dep pyflakes
+  * fix pyflakes
+  * build-dep requires python-coverage
+  * switch from nosetests to the builtin test runner Just because it
+    doesn't seem to currently use any feature of nosetest and this way
+    we can reduce the dependencies by one. We'll feel free to switch it
+    back to using nosetests (and add the dep to debian/control this
+    time) if we want to use some extra feature of nose.
+  * remove test failure which was inserted just to see if hudson was
+    running the tests properly
+  * Fix bug inserted by pyflakes inspired cleanup earlier -- it turns
+    out that _get_version() wasn't dead code but was having a necessary
+    side-effect. Add new method named _check_version() to be clearer
+    about what it does compared to _get_version()
+  * port tests from mox to mock because they were failing and I don't
+    understand mox. now they are working. add mock to the python and
+    debian deps. remove apparently left-over line of code that runs
+    "sign()" at the end of checking a signature
+
+  [ SimpleGeo Nerds ]
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Tue, 04 Jan 2011 19:39:34 +0000
+
+python-oauth2 (1.2.1) lucid; urgency=low
+
+  [ Joe Stump ]
+  * Fixed a minor spelling error, added tests for
+    oauth2.Error.__str__(), and added a test for build_xoauth_string().
+  * Added a 'make test' command to the Makefile.
+  * Added a bunch of new tests. 100% coverage has lapsed a bit. Fixing
+    now.
+
+  [ Ian Eure ]
+  * Update deb rule in makefile to sign packages and move them to
+    dist/deb.
+
+  [ Joe Stump ]
+  * Implemented a 'fix' for a bug in certain OAuth providers.
+    http://bit.ly/aUrH43
+
+  [ Ian Eure ]
+  * Update packaging.
+  * Fix version disparity.
+
+  [ coulix ]
+  * get normalised parameters should not take into account oauth_ params
+
+  [ Mathias Herberts ]
+  * Modified get_normalized_parameters so it does not encode '~' for
+    full conformance with RFC 5849 section 3.6
+
+  [ Peter Bengtsson ]
+  * made it python 2.4 compatible
+
+  [ Roderic Campbell ]
+  * tok != token. con != consumer. May be confusing for people not
+    actually reading the code.
+
+  [ Ian Eure ]
+
+ -- Ian Eure <ian@simplegeo.com>  Mon, 28 Jun 2010 12:02:02 -0700
+
+python-oauth2 (1.2.1pre2) unstable; urgency=low
+
+  * Fix version disparity between setup.py & debian/changelog.
+
+ -- Ian Eure <ian@simplegeo.com>  Fri, 11 Jun 2010 16:11:41 -0700
+
+python-oauth2 (1.2.1pre1) unstable; urgency=low
+
+  * Make a native package.
+  * Increment version.
+  * Make debian/rules executable.
+  * Update standards-version, maintainers.
+  * Remvoe unneeded python-central build dependency.
+
+ -- Ian Eure <ian@simplegeo.com>  Fri, 11 Jun 2010 16:09:01 -0700
+
+python-oauth2 (1.0.0-1simplegeo01) karmic; urgency=low
+
+  * Initial build for SimpleGeo
+
+ -- SimpleGeo Nerds <nerds@simplegeo.com>  Wed, 21 Oct 2009 23:24:00 -0700

+ 1 - 0
desktop/core/ext-py/python-oauth2/debian/compat

@@ -0,0 +1 @@
+5

+ 24 - 0
desktop/core/ext-py/python-oauth2/debian/control

@@ -0,0 +1,24 @@
+Source: python-oauth2
+Section: python
+Priority: optional
+Maintainer: SimpleGeo Nerds <nerds@simplegeo.com>
+Uploaders: Chris Lea <chris.lea@gmail.com>, Ian Eure <ian@simplegeo.com>
+Standards-Version: 3.8.4
+XS-Python-Version: all
+Build-Depends: debhelper (>= 4.1.13), cdbs (>= 0.4.49), python, python-setuptools, python-support, pyflakes, python-coverage (>= 2.85), python-mock
+Homepage: http://github.com/simplegeo/python-oauth2
+
+Package: python-oauth2
+Architecture: all
+XB-Python-Version: ${python:Versions}
+Depends: ${python:Depends}, python-httplib2
+Provides: ${python:Provides}
+Suggests:
+Description: A Python OAuth class with several useful
+ features.
+ .
+ * 100% unit test coverage.
+ * The Request class now extends from dict.
+ * The Client class works and extends from httplib2.
+   It's a thin wrapper that handles automatically signing
+   any normal HTTP request you might wish to make.

+ 21 - 0
desktop/core/ext-py/python-oauth2/debian/copyright

@@ -0,0 +1,21 @@
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 1 - 0
desktop/core/ext-py/python-oauth2/debian/pycompat

@@ -0,0 +1 @@
+2

+ 1 - 0
desktop/core/ext-py/python-oauth2/debian/pyversions

@@ -0,0 +1 @@
+2.5-

+ 12 - 0
desktop/core/ext-py/python-oauth2/debian/rules

@@ -0,0 +1,12 @@
+#!/usr/bin/make -f
+# -*- mode: makefile; coding: utf-8 -*-
+
+DEB_PYTHON_SYSTEM = pysupport
+
+include /usr/share/cdbs/1/rules/debhelper.mk
+include /usr/share/cdbs/1/class/python-distutils.mk
+
+build/python-oauth2::
+	pyflakes oauth2
+	python-coverage run --branch --include=oauth2/* setup.py test
+	python-coverage html -d $(ARTIFACTS)/htmlcov

+ 108 - 0
desktop/core/ext-py/python-oauth2/example/appengine_oauth.py

@@ -0,0 +1,108 @@
+"""
+The MIT License
+
+Copyright (c) 2010 Justin Plock
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import os
+
+from google.appengine.ext import webapp
+from google.appengine.ext import db
+from google.appengine.ext.webapp import util
+import oauth2 as oauth # httplib2 is required for this to work on AppEngine
+
+class Client(db.Model):
+    # oauth_key is the Model's key_name field
+    oauth_secret = db.StringProperty() # str(uuid.uuid4()) works well for this
+    first_name = db.StringProperty()
+    last_name = db.StringProperty()
+    email_address = db.EmailProperty(required=True)
+    password = db.StringProperty(required=True)
+
+    @property
+    def secret(self):
+        return self.oauth_secret
+
+class OAuthHandler(webapp.RequestHandler):
+
+    def __init__(self):
+        self._server = oauth.Server()
+        self._server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        self._server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
+
+    def get_oauth_request(self):
+        """Return an OAuth Request object for the current request."""
+
+        try:
+            method = os.environ['REQUEST_METHOD']
+        except:
+            method = 'GET'
+
+        postdata = None
+        if method in ('POST', 'PUT'):
+            postdata = self.request.body
+
+        return oauth.Request.from_request(method, self.request.uri,
+            headers=self.request.headers, query_string=postdata)
+
+    def get_client(self, request=None):
+        """Return the client from the OAuth parameters."""
+
+        if not isinstance(request, oauth.Request):
+            request = self.get_oauth_request()
+        client_key = request.get_parameter('oauth_consumer_key')
+        if not client_key:
+            raise Exception('Missing "oauth_consumer_key" parameter in ' \
+                'OAuth "Authorization" header')
+
+        client = models.Client.get_by_key_name(client_key)
+        if not client:
+            raise Exception('Client "%s" not found.' % client_key)
+
+        return client
+
+    def is_valid(self):
+        """Returns a Client object if this is a valid OAuth request."""
+
+        try:
+            request = self.get_oauth_request()
+            client = self.get_client(request)
+            params = self._server.verify_request(request, client, None)
+        except Exception, e:
+            raise e
+
+        return client
+
+class SampleHandler(OAuthHandler):
+    def get(self):
+        try:
+            client = self.is_valid()
+        except Exception, e:
+            self.error(500)
+            self.response.out.write(e)
+
+def main():
+    application = webapp.WSGIApplication([(r'/sample', SampleHandler)],
+        debug=False)
+    util.run_wsgi_app(application)
+
+if __name__ == '__main__':
+    main()

+ 165 - 0
desktop/core/ext-py/python-oauth2/example/client.py

@@ -0,0 +1,165 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+Example consumer. This is not recommended for production.
+Instead, you'll want to create your own subclass of OAuthClient
+or find one that works with your web framework.
+"""
+
+import httplib
+import time
+import oauth.oauth as oauth
+
+# settings for the local test consumer
+SERVER = 'localhost'
+PORT = 8080
+
+# fake urls for the test server (matches ones in server.py)
+REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
+ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
+AUTHORIZATION_URL = 'https://photos.example.net/authorize'
+CALLBACK_URL = 'http://printer.example.com/request_token_ready'
+RESOURCE_URL = 'http://photos.example.net/photos'
+
+# key and secret granted by the service provider for this consumer application - same as the MockOAuthDataStore
+CONSUMER_KEY = 'key'
+CONSUMER_SECRET = 'secret'
+
+# example client using httplib with headers
+class SimpleOAuthClient(oauth.OAuthClient):
+
+    def __init__(self, server, port=httplib.HTTP_PORT, request_token_url='', access_token_url='', authorization_url=''):
+        self.server = server
+        self.port = port
+        self.request_token_url = request_token_url
+        self.access_token_url = access_token_url
+        self.authorization_url = authorization_url
+        self.connection = httplib.HTTPConnection("%s:%d" % (self.server, self.port))
+
+    def fetch_request_token(self, oauth_request):
+        # via headers
+        # -> OAuthToken
+        self.connection.request(oauth_request.http_method, self.request_token_url, headers=oauth_request.to_header()) 
+        response = self.connection.getresponse()
+        return oauth.OAuthToken.from_string(response.read())
+
+    def fetch_access_token(self, oauth_request):
+        # via headers
+        # -> OAuthToken
+        self.connection.request(oauth_request.http_method, self.access_token_url, headers=oauth_request.to_header()) 
+        response = self.connection.getresponse()
+        return oauth.OAuthToken.from_string(response.read())
+
+    def authorize_token(self, oauth_request):
+        # via url
+        # -> typically just some okay response
+        self.connection.request(oauth_request.http_method, oauth_request.to_url()) 
+        response = self.connection.getresponse()
+        return response.read()
+
+    def access_resource(self, oauth_request):
+        # via post body
+        # -> some protected resources
+        headers = {'Content-Type' :'application/x-www-form-urlencoded'}
+        self.connection.request('POST', RESOURCE_URL, body=oauth_request.to_postdata(), headers=headers)
+        response = self.connection.getresponse()
+        return response.read()
+
+def run_example():
+
+    # setup
+    print '** OAuth Python Library Example **'
+    client = SimpleOAuthClient(SERVER, PORT, REQUEST_TOKEN_URL, ACCESS_TOKEN_URL, AUTHORIZATION_URL)
+    consumer = oauth.OAuthConsumer(CONSUMER_KEY, CONSUMER_SECRET)
+    signature_method_plaintext = oauth.OAuthSignatureMethod_PLAINTEXT()
+    signature_method_hmac_sha1 = oauth.OAuthSignatureMethod_HMAC_SHA1()
+    pause()
+
+    # get request token
+    print '* Obtain a request token ...'
+    pause()
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, callback=CALLBACK_URL, http_url=client.request_token_url)
+    oauth_request.sign_request(signature_method_plaintext, consumer, None)
+    print 'REQUEST (via headers)'
+    print 'parameters: %s' % str(oauth_request.parameters)
+    pause()
+    token = client.fetch_request_token(oauth_request)
+    print 'GOT'
+    print 'key: %s' % str(token.key)
+    print 'secret: %s' % str(token.secret)
+    print 'callback confirmed? %s' % str(token.callback_confirmed)
+    pause()
+
+    print '* Authorize the request token ...'
+    pause()
+    oauth_request = oauth.OAuthRequest.from_token_and_callback(token=token, http_url=client.authorization_url)
+    print 'REQUEST (via url query string)'
+    print 'parameters: %s' % str(oauth_request.parameters)
+    pause()
+    # this will actually occur only on some callback
+    response = client.authorize_token(oauth_request)
+    print 'GOT'
+    print response
+    # sad way to get the verifier
+    import urlparse, cgi
+    query = urlparse.urlparse(response)[4]
+    params = cgi.parse_qs(query, keep_blank_values=False)
+    verifier = params['oauth_verifier'][0]
+    print 'verifier: %s' % verifier
+    pause()
+
+    # get access token
+    print '* Obtain an access token ...'
+    pause()
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, verifier=verifier, http_url=client.access_token_url)
+    oauth_request.sign_request(signature_method_plaintext, consumer, token)
+    print 'REQUEST (via headers)'
+    print 'parameters: %s' % str(oauth_request.parameters)
+    pause()
+    token = client.fetch_access_token(oauth_request)
+    print 'GOT'
+    print 'key: %s' % str(token.key)
+    print 'secret: %s' % str(token.secret)
+    pause()
+
+    # access some protected resources
+    print '* Access protected resources ...'
+    pause()
+    parameters = {'file': 'vacation.jpg', 'size': 'original'} # resource specific params
+    oauth_request = oauth.OAuthRequest.from_consumer_and_token(consumer, token=token, http_method='POST', http_url=RESOURCE_URL, parameters=parameters)
+    oauth_request.sign_request(signature_method_hmac_sha1, consumer, token)
+    print 'REQUEST (via post body)'
+    print 'parameters: %s' % str(oauth_request.parameters)
+    pause()
+    params = client.access_resource(oauth_request)
+    print 'GOT'
+    print 'non-oauth parameters: %s' % params
+    pause()
+
+def pause():
+    print ''
+    time.sleep(1)
+
+if __name__ == '__main__':
+    run_example()
+    print 'Done.'

+ 195 - 0
desktop/core/ext-py/python-oauth2/example/server.py

@@ -0,0 +1,195 @@
+"""
+The MIT License
+
+Copyright (c) 2007 Leah Culver
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
+import urllib
+
+import oauth.oauth as oauth
+
+# fake urls for the test server
+REQUEST_TOKEN_URL = 'https://photos.example.net/request_token'
+ACCESS_TOKEN_URL = 'https://photos.example.net/access_token'
+AUTHORIZATION_URL = 'https://photos.example.net/authorize'
+CALLBACK_URL = 'http://printer.example.com/request_token_ready'
+RESOURCE_URL = 'http://photos.example.net/photos'
+REALM = 'http://photos.example.net/'
+VERIFIER = 'verifier'
+
+# example store for one of each thing
+class MockOAuthDataStore(oauth.OAuthDataStore):
+
+    def __init__(self):
+        self.consumer = oauth.OAuthConsumer('key', 'secret')
+        self.request_token = oauth.OAuthToken('requestkey', 'requestsecret')
+        self.access_token = oauth.OAuthToken('accesskey', 'accesssecret')
+        self.nonce = 'nonce'
+        self.verifier = VERIFIER
+
+    def lookup_consumer(self, key):
+        if key == self.consumer.key:
+            return self.consumer
+        return None
+
+    def lookup_token(self, token_type, token):
+        token_attrib = getattr(self, '%s_token' % token_type)
+        if token == token_attrib.key:
+            ## HACK
+            token_attrib.set_callback(CALLBACK_URL)
+            return token_attrib
+        return None
+
+    def lookup_nonce(self, oauth_consumer, oauth_token, nonce):
+        if oauth_token and oauth_consumer.key == self.consumer.key and (oauth_token.key == self.request_token.key or oauth_token.key == self.access_token.key) and nonce == self.nonce:
+            return self.nonce
+        return None
+
+    def fetch_request_token(self, oauth_consumer, oauth_callback):
+        if oauth_consumer.key == self.consumer.key:
+            if oauth_callback:
+                # want to check here if callback is sensible
+                # for mock store, we assume it is
+                self.request_token.set_callback(oauth_callback)
+            return self.request_token
+        return None
+
+    def fetch_access_token(self, oauth_consumer, oauth_token, oauth_verifier):
+        if oauth_consumer.key == self.consumer.key and oauth_token.key == self.request_token.key and oauth_verifier == self.verifier:
+            # want to check here if token is authorized
+            # for mock store, we assume it is
+            return self.access_token
+        return None
+
+    def authorize_request_token(self, oauth_token, user):
+        if oauth_token.key == self.request_token.key:
+            # authorize the request token in the store
+            # for mock store, do nothing
+            return self.request_token
+        return None
+
+class RequestHandler(BaseHTTPRequestHandler):
+
+    def __init__(self, *args, **kwargs):
+        self.oauth_server = oauth.OAuthServer(MockOAuthDataStore())
+        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_PLAINTEXT())
+        self.oauth_server.add_signature_method(oauth.OAuthSignatureMethod_HMAC_SHA1())
+        BaseHTTPRequestHandler.__init__(self, *args, **kwargs)
+
+    # example way to send an oauth error
+    def send_oauth_error(self, err=None):
+        # send a 401 error
+        self.send_error(401, str(err.message))
+        # return the authenticate header
+        header = oauth.build_authenticate_header(realm=REALM)
+        for k, v in header.iteritems():
+            self.send_header(k, v) 
+
+    def do_GET(self):
+
+        # debug info
+        #print self.command, self.path, self.headers
+        
+        # get the post data (if any)
+        postdata = None
+        if self.command == 'POST':
+            try:
+                length = int(self.headers.getheader('content-length'))
+                postdata = self.rfile.read(length)
+            except:
+                pass
+
+        # construct the oauth request from the request parameters
+        oauth_request = oauth.OAuthRequest.from_request(self.command, self.path, headers=self.headers, query_string=postdata)
+
+        # request token
+        if self.path.startswith(REQUEST_TOKEN_URL):
+            try:
+                # create a request token
+                token = self.oauth_server.fetch_request_token(oauth_request)
+                # send okay response
+                self.send_response(200, 'OK')
+                self.end_headers()
+                # return the token
+                self.wfile.write(token.to_string())
+            except oauth.OAuthError, err:
+                self.send_oauth_error(err)
+            return
+
+        # user authorization
+        if self.path.startswith(AUTHORIZATION_URL):
+            try:
+                # get the request token
+                token = self.oauth_server.fetch_request_token(oauth_request)
+                # authorize the token (kind of does nothing for now)
+                token = self.oauth_server.authorize_token(token, None)
+                token.set_verifier(VERIFIER)
+                # send okay response
+                self.send_response(200, 'OK')
+                self.end_headers()
+                # return the callback url (to show server has it)
+                self.wfile.write(token.get_callback_url())
+            except oauth.OAuthError, err:
+                self.send_oauth_error(err)
+            return
+
+        # access token
+        if self.path.startswith(ACCESS_TOKEN_URL):
+            try:
+                # create an access token
+                token = self.oauth_server.fetch_access_token(oauth_request)
+                # send okay response
+                self.send_response(200, 'OK')
+                self.end_headers()
+                # return the token
+                self.wfile.write(token.to_string())
+            except oauth.OAuthError, err:
+                self.send_oauth_error(err)
+            return
+
+        # protected resources
+        if self.path.startswith(RESOURCE_URL):
+            try:
+                # verify the request has been oauth authorized
+                consumer, token, params = self.oauth_server.verify_request(oauth_request)
+                # send okay response
+                self.send_response(200, 'OK')
+                self.end_headers()
+                # return the extra parameters - just for something to return
+                self.wfile.write(str(params))
+            except oauth.OAuthError, err:
+                self.send_oauth_error(err)
+            return
+
+    def do_POST(self):
+        return self.do_GET()
+
+def main():
+    try:
+        server = HTTPServer(('', 8080), RequestHandler)
+        print 'Test server running...'
+        server.serve_forever()
+    except KeyboardInterrupt:
+        server.socket.close()
+
+if __name__ == '__main__':
+    main()

+ 860 - 0
desktop/core/ext-py/python-oauth2/oauth2/__init__.py

@@ -0,0 +1,860 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import base64
+import urllib
+import time
+import random
+import urlparse
+import hmac
+import binascii
+import httplib2
+
+try:
+    from urlparse import parse_qs
+    parse_qs # placate pyflakes
+except ImportError:
+    # fall back for Python 2.5
+    from cgi import parse_qs
+
+try:
+    from hashlib import sha1
+    sha = sha1
+except ImportError:
+    # hashlib was added in Python 2.5
+    import sha
+
+import _version
+
+__version__ = _version.__version__
+
+OAUTH_VERSION = '1.0'  # Hi Blaine!
+HTTP_METHOD = 'GET'
+SIGNATURE_METHOD = 'PLAINTEXT'
+
+
+class Error(RuntimeError):
+    """Generic exception class."""
+
+    def __init__(self, message='OAuth error occurred.'):
+        self._message = message
+
+    @property
+    def message(self):
+        """A hack to get around the deprecation errors in 2.6."""
+        return self._message
+
+    def __str__(self):
+        return self._message
+
+
+class MissingSignature(Error):
+    pass
+
+
+def build_authenticate_header(realm=''):
+    """Optional WWW-Authenticate header (401 error)"""
+    return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+
+def build_xoauth_string(url, consumer, token=None):
+    """Build an XOAUTH string for use in SMTP/IMPA authentication."""
+    request = Request.from_consumer_and_token(consumer, token,
+        "GET", url)
+
+    signing_method = SignatureMethod_HMAC_SHA1()
+    request.sign_request(signing_method, consumer, token)
+
+    params = []
+    for k, v in sorted(request.iteritems()):
+        if v is not None:
+            params.append('%s="%s"' % (k, escape(v)))
+
+    return "%s %s %s" % ("GET", url, ','.join(params))
+
+
+def to_unicode(s):
+    """ Convert to unicode, raise exception with instructive error
+    message if s is not unicode, ascii, or utf-8. """
+    if not isinstance(s, unicode):
+        if not isinstance(s, str):
+            raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s))
+        try:
+            s = s.decode('utf-8')
+        except UnicodeDecodeError, le:
+            raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,))
+    return s
+
+def to_utf8(s):
+    return to_unicode(s).encode('utf-8')
+
+def to_unicode_if_string(s):
+    if isinstance(s, basestring):
+        return to_unicode(s)
+    else:
+        return s
+
+def to_utf8_if_string(s):
+    if isinstance(s, basestring):
+        return to_utf8(s)
+    else:
+        return s
+
+def to_unicode_optional_iterator(x):
+    """
+    Raise TypeError if x is a str containing non-utf8 bytes or if x is
+    an iterable which contains such a str.
+    """
+    if isinstance(x, basestring):
+        return to_unicode(x)
+
+    try:
+        l = list(x)
+    except TypeError, e:
+        assert 'is not iterable' in str(e)
+        return x
+    else:
+        return [ to_unicode(e) for e in l ]
+
+def to_utf8_optional_iterator(x):
+    """
+    Raise TypeError if x is a str or if x is an iterable which
+    contains a str.
+    """
+    if isinstance(x, basestring):
+        return to_utf8(x)
+
+    try:
+        l = list(x)
+    except TypeError, e:
+        assert 'is not iterable' in str(e)
+        return x
+    else:
+        return [ to_utf8_if_string(e) for e in l ]
+
+def escape(s):
+    """Escape a URL including any /."""
+    return urllib.quote(s.encode('utf-8'), safe='~')
+
+def generate_timestamp():
+    """Get seconds since epoch (UTC)."""
+    return int(time.time())
+
+
+def generate_nonce(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+def generate_verifier(length=8):
+    """Generate pseudorandom number."""
+    return ''.join([str(random.randint(0, 9)) for i in range(length)])
+
+
+class Consumer(object):
+    """A consumer of OAuth-protected services.
+ 
+    The OAuth consumer is a "third-party" service that wants to access
+    protected resources from an OAuth service provider on behalf of an end
+    user. It's kind of the OAuth client.
+ 
+    Usually a consumer must be registered with the service provider by the
+    developer of the consumer software. As part of that process, the service
+    provider gives the consumer a *key* and a *secret* with which the consumer
+    software can identify itself to the service. The consumer will include its
+    key in each request to identify itself, but will use its secret only when
+    signing requests, to prove that the request is from that particular
+    registered consumer.
+ 
+    Once registered, the consumer can then use its consumer credentials to ask
+    the service provider for a request token, kicking off the OAuth
+    authorization process.
+    """
+
+    key = None
+    secret = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+        if self.key is None or self.secret is None:
+            raise ValueError("Key and secret must be set.")
+
+    def __str__(self):
+        data = {'oauth_consumer_key': self.key,
+            'oauth_consumer_secret': self.secret}
+
+        return urllib.urlencode(data)
+
+
+class Token(object):
+    """An OAuth credential used to request authorization or a protected
+    resource.
+ 
+    Tokens in OAuth comprise a *key* and a *secret*. The key is included in
+    requests to identify the token being used, but the secret is used only in
+    the signature, to prove that the requester is who the server gave the
+    token to.
+ 
+    When first negotiating the authorization, the consumer asks for a *request
+    token* that the live user authorizes with the service provider. The
+    consumer then exchanges the request token for an *access token* that can
+    be used to access protected resources.
+    """
+
+    key = None
+    secret = None
+    callback = None
+    callback_confirmed = None
+    verifier = None
+
+    def __init__(self, key, secret):
+        self.key = key
+        self.secret = secret
+
+        if self.key is None or self.secret is None:
+            raise ValueError("Key and secret must be set.")
+
+    def set_callback(self, callback):
+        self.callback = callback
+        self.callback_confirmed = 'true'
+
+    def set_verifier(self, verifier=None):
+        if verifier is not None:
+            self.verifier = verifier
+        else:
+            self.verifier = generate_verifier()
+
+    def get_callback_url(self):
+        if self.callback and self.verifier:
+            # Append the oauth_verifier.
+            parts = urlparse.urlparse(self.callback)
+            scheme, netloc, path, params, query, fragment = parts[:6]
+            if query:
+                query = '%s&oauth_verifier=%s' % (query, self.verifier)
+            else:
+                query = 'oauth_verifier=%s' % self.verifier
+            return urlparse.urlunparse((scheme, netloc, path, params,
+                query, fragment))
+        return self.callback
+
+    def to_string(self):
+        """Returns this token as a plain string, suitable for storage.
+ 
+        The resulting string includes the token's secret, so you should never
+        send or store this string where a third party can read it.
+        """
+
+        data = {
+            'oauth_token': self.key,
+            'oauth_token_secret': self.secret,
+        }
+
+        if self.callback_confirmed is not None:
+            data['oauth_callback_confirmed'] = self.callback_confirmed
+        return urllib.urlencode(data)
+ 
+    @staticmethod
+    def from_string(s):
+        """Deserializes a token from a string like one returned by
+        `to_string()`."""
+
+        if not len(s):
+            raise ValueError("Invalid parameter string.")
+
+        params = parse_qs(s, keep_blank_values=False)
+        if not len(params):
+            raise ValueError("Invalid parameter string.")
+
+        try:
+            key = params['oauth_token'][0]
+        except Exception:
+            raise ValueError("'oauth_token' not found in OAuth request.")
+
+        try:
+            secret = params['oauth_token_secret'][0]
+        except Exception:
+            raise ValueError("'oauth_token_secret' not found in " 
+                "OAuth request.")
+
+        token = Token(key, secret)
+        try:
+            token.callback_confirmed = params['oauth_callback_confirmed'][0]
+        except KeyError:
+            pass  # 1.0, no callback confirmed.
+        return token
+
+    def __str__(self):
+        return self.to_string()
+
+
+def setter(attr):
+    name = attr.__name__
+ 
+    def getter(self):
+        try:
+            return self.__dict__[name]
+        except KeyError:
+            raise AttributeError(name)
+ 
+    def deleter(self):
+        del self.__dict__[name]
+ 
+    return property(getter, attr, deleter)
+
+
+class Request(dict):
+ 
+    """The parameters and information for an HTTP request, suitable for
+    authorizing with OAuth credentials.
+ 
+    When a consumer wants to access a service's protected resources, it does
+    so using a signed HTTP request identifying itself (the consumer) with its
+    key, and providing an access token authorized by the end user to access
+    those resources.
+ 
+    """
+ 
+    version = OAUTH_VERSION
+
+    def __init__(self, method=HTTP_METHOD, url=None, parameters=None,
+                 body='', is_form_encoded=False):
+        if url is not None:
+            self.url = to_unicode(url)
+        self.method = method
+        if parameters is not None:
+            for k, v in parameters.iteritems():
+                k = to_unicode(k)
+                v = to_unicode_optional_iterator(v)
+                self[k] = v
+        self.body = body
+        self.is_form_encoded = is_form_encoded
+
+
+    @setter
+    def url(self, value):
+        self.__dict__['url'] = value
+        if value is not None:
+            scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
+
+            # Exclude default port numbers.
+            if scheme == 'http' and netloc[-3:] == ':80':
+                netloc = netloc[:-3]
+            elif scheme == 'https' and netloc[-4:] == ':443':
+                netloc = netloc[:-4]
+            if scheme not in ('http', 'https'):
+                raise ValueError("Unsupported URL %s (%s)." % (value, scheme))
+
+            # Normalized URL excludes params, query, and fragment.
+            self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None))
+        else:
+            self.normalized_url = None
+            self.__dict__['url'] = None
+ 
+    @setter
+    def method(self, value):
+        self.__dict__['method'] = value.upper()
+ 
+    def _get_timestamp_nonce(self):
+        return self['oauth_timestamp'], self['oauth_nonce']
+ 
+    def get_nonoauth_parameters(self):
+        """Get any non-OAuth parameters."""
+        return dict([(k, v) for k, v in self.iteritems() 
+                    if not k.startswith('oauth_')])
+ 
+    def to_header(self, realm=''):
+        """Serialize as a header for an HTTPAuth request."""
+        oauth_params = ((k, v) for k, v in self.items() 
+                            if k.startswith('oauth_'))
+        stringy_params = ((k, escape(str(v))) for k, v in oauth_params)
+        header_params = ('%s="%s"' % (k, v) for k, v in stringy_params)
+        params_header = ', '.join(header_params)
+ 
+        auth_header = 'OAuth realm="%s"' % realm
+        if params_header:
+            auth_header = "%s, %s" % (auth_header, params_header)
+ 
+        return {'Authorization': auth_header}
+ 
+    def to_postdata(self):
+        """Serialize as post data for a POST request."""
+        d = {}
+        for k, v in self.iteritems():
+            d[k.encode('utf-8')] = to_utf8_optional_iterator(v)
+
+        # tell urlencode to deal with sequence values and map them correctly
+        # to resulting querystring. for example self["k"] = ["v1", "v2"] will
+        # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D
+        return urllib.urlencode(d, True).replace('+', '%20')
+ 
+    def to_url(self):
+        """Serialize as a URL for a GET request."""
+        base_url = urlparse.urlparse(self.url)
+        try:
+            query = base_url.query
+        except AttributeError:
+            # must be python <2.5
+            query = base_url[4]
+        query = parse_qs(query)
+        for k, v in self.items():
+            query.setdefault(k, []).append(v)
+        
+        try:
+            scheme = base_url.scheme
+            netloc = base_url.netloc
+            path = base_url.path
+            params = base_url.params
+            fragment = base_url.fragment
+        except AttributeError:
+            # must be python <2.5
+            scheme = base_url[0]
+            netloc = base_url[1]
+            path = base_url[2]
+            params = base_url[3]
+            fragment = base_url[5]
+        
+        url = (scheme, netloc, path, params,
+               urllib.urlencode(query, True), fragment)
+        return urlparse.urlunparse(url)
+
+    def get_parameter(self, parameter):
+        ret = self.get(parameter)
+        if ret is None:
+            raise Error('Parameter not found: %s' % parameter)
+
+        return ret
+
+    def get_normalized_parameters(self):
+        """Return a string that contains the parameters that must be signed."""
+        items = []
+        for key, value in self.iteritems():
+            if key == 'oauth_signature':
+                continue
+            # 1.0a/9.1.1 states that kvp must be sorted by key, then by value,
+            # so we unpack sequence values into multiple items for sorting.
+            if isinstance(value, basestring):
+                items.append((to_utf8_if_string(key), to_utf8(value)))
+            else:
+                try:
+                    value = list(value)
+                except TypeError, e:
+                    assert 'is not iterable' in str(e)
+                    items.append((to_utf8_if_string(key), to_utf8_if_string(value)))
+                else:
+                    items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value)
+
+        # Include any query string parameters from the provided URL
+        query = urlparse.urlparse(self.url)[4]
+
+        url_items = self._split_url_string(query).items()
+        url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ]
+        items.extend(url_items)
+
+        items.sort()
+        encoded_str = urllib.urlencode(items)
+        # Encode signature parameters per Oauth Core 1.0 protocol
+        # spec draft 7, section 3.6
+        # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6)
+        # Spaces must be encoded with "%20" instead of "+"
+        return encoded_str.replace('+', '%20').replace('%7E', '~')
+
+    def sign_request(self, signature_method, consumer, token):
+        """Set the signature parameter to the result of sign."""
+
+        if not self.is_form_encoded:
+            # according to
+            # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+            # section 4.1.1 "OAuth Consumers MUST NOT include an
+            # oauth_body_hash parameter on requests with form-encoded
+            # request bodies."
+            self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest())
+
+        if 'oauth_consumer_key' not in self:
+            self['oauth_consumer_key'] = consumer.key
+
+        if token and 'oauth_token' not in self:
+            self['oauth_token'] = token.key
+
+        self['oauth_signature_method'] = signature_method.name
+        self['oauth_signature'] = signature_method.sign(self, consumer, token)
+ 
+    @classmethod
+    def make_timestamp(cls):
+        """Get seconds since epoch (UTC)."""
+        return str(int(time.time()))
+ 
+    @classmethod
+    def make_nonce(cls):
+        """Generate pseudorandom number."""
+        return str(random.randint(0, 100000000))
+ 
+    @classmethod
+    def from_request(cls, http_method, http_url, headers=None, parameters=None,
+            query_string=None):
+        """Combines multiple parameter sources."""
+        if parameters is None:
+            parameters = {}
+ 
+        # Headers
+        if headers and 'Authorization' in headers:
+            auth_header = headers['Authorization']
+            # Check that the authorization header is OAuth.
+            if auth_header[:6] == 'OAuth ':
+                auth_header = auth_header[6:]
+                try:
+                    # Get the parameters from the header.
+                    header_params = cls._split_header(auth_header)
+                    parameters.update(header_params)
+                except:
+                    raise Error('Unable to parse OAuth parameters from '
+                        'Authorization header.')
+ 
+        # GET or POST query string.
+        if query_string:
+            query_params = cls._split_url_string(query_string)
+            parameters.update(query_params)
+ 
+        # URL parameters.
+        param_str = urlparse.urlparse(http_url)[4] # query
+        url_params = cls._split_url_string(param_str)
+        parameters.update(url_params)
+ 
+        if parameters:
+            return cls(http_method, http_url, parameters)
+ 
+        return None
+ 
+    @classmethod
+    def from_consumer_and_token(cls, consumer, token=None,
+            http_method=HTTP_METHOD, http_url=None, parameters=None,
+            body='', is_form_encoded=False):
+        if not parameters:
+            parameters = {}
+ 
+        defaults = {
+            'oauth_consumer_key': consumer.key,
+            'oauth_timestamp': cls.make_timestamp(),
+            'oauth_nonce': cls.make_nonce(),
+            'oauth_version': cls.version,
+        }
+ 
+        defaults.update(parameters)
+        parameters = defaults
+ 
+        if token:
+            parameters['oauth_token'] = token.key
+            if token.verifier:
+                parameters['oauth_verifier'] = token.verifier
+ 
+        return Request(http_method, http_url, parameters, body=body, 
+                       is_form_encoded=is_form_encoded)
+ 
+    @classmethod
+    def from_token_and_callback(cls, token, callback=None, 
+        http_method=HTTP_METHOD, http_url=None, parameters=None):
+
+        if not parameters:
+            parameters = {}
+ 
+        parameters['oauth_token'] = token.key
+ 
+        if callback:
+            parameters['oauth_callback'] = callback
+ 
+        return cls(http_method, http_url, parameters)
+ 
+    @staticmethod
+    def _split_header(header):
+        """Turn Authorization: header into parameters."""
+        params = {}
+        parts = header.split(',')
+        for param in parts:
+            # Ignore realm parameter.
+            if param.find('realm') > -1:
+                continue
+            # Remove whitespace.
+            param = param.strip()
+            # Split key-value.
+            param_parts = param.split('=', 1)
+            # Remove quotes and unescape the value.
+            params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"'))
+        return params
+ 
+    @staticmethod
+    def _split_url_string(param_str):
+        """Turn URL string into parameters."""
+        parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True)
+        for k, v in parameters.iteritems():
+            parameters[k] = urllib.unquote(v[0])
+        return parameters
+
+
+class Client(httplib2.Http):
+    """OAuthClient is a worker to attempt to execute a request."""
+
+    def __init__(self, consumer, token=None, cache=None, timeout=None,
+        proxy_info=None):
+
+        if consumer is not None and not isinstance(consumer, Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, Token):
+            raise ValueError("Invalid token.")
+
+        self.consumer = consumer
+        self.token = token
+        self.method = SignatureMethod_HMAC_SHA1()
+
+        httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info)
+
+    def set_signature_method(self, method):
+        if not isinstance(method, SignatureMethod):
+            raise ValueError("Invalid signature method.")
+
+        self.method = method
+
+    def request(self, uri, method="GET", body='', headers=None, 
+        redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None):
+        DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded'
+
+        if not isinstance(headers, dict):
+            headers = {}
+
+        if method == "POST":
+            headers['Content-Type'] = headers.get('Content-Type', 
+                DEFAULT_POST_CONTENT_TYPE)
+
+        is_form_encoded = \
+            headers.get('Content-Type') == 'application/x-www-form-urlencoded'
+
+        if is_form_encoded and body:
+            parameters = parse_qs(body)
+        else:
+            parameters = None
+
+        req = Request.from_consumer_and_token(self.consumer, 
+            token=self.token, http_method=method, http_url=uri, 
+            parameters=parameters, body=body, is_form_encoded=is_form_encoded)
+
+        req.sign_request(self.method, self.consumer, self.token)
+
+        schema, rest = urllib.splittype(uri)
+        if rest.startswith('//'):
+            hierpart = '//'
+        else:
+            hierpart = ''
+        host, rest = urllib.splithost(rest)
+
+        realm = schema + ':' + hierpart + host
+
+        if is_form_encoded:
+            body = req.to_postdata()
+        elif method == "GET":
+            uri = req.to_url()
+        else:
+            headers.update(req.to_header(realm=realm))
+
+        return httplib2.Http.request(self, uri, method=method, body=body,
+            headers=headers, redirections=redirections,
+            connection_type=connection_type)
+
+
+class Server(object):
+    """A skeletal implementation of a service provider, providing protected
+    resources to requests from authorized consumers.
+ 
+    This class implements the logic to check requests for authorization. You
+    can use it with your web server or web framework to protect certain
+    resources with OAuth.
+    """
+
+    timestamp_threshold = 300 # In seconds, five minutes.
+    version = OAUTH_VERSION
+    signature_methods = None
+
+    def __init__(self, signature_methods=None):
+        self.signature_methods = signature_methods or {}
+
+    def add_signature_method(self, signature_method):
+        self.signature_methods[signature_method.name] = signature_method
+        return self.signature_methods
+
+    def verify_request(self, request, consumer, token):
+        """Verifies an api call and checks all the parameters."""
+
+        self._check_version(request)
+        self._check_signature(request, consumer, token)
+        parameters = request.get_nonoauth_parameters()
+        return parameters
+
+    def build_authenticate_header(self, realm=''):
+        """Optional support for the authenticate header."""
+        return {'WWW-Authenticate': 'OAuth realm="%s"' % realm}
+
+    def _check_version(self, request):
+        """Verify the correct version of the request for this server."""
+        version = self._get_version(request)
+        if version and version != self.version:
+            raise Error('OAuth version %s not supported.' % str(version))
+
+    def _get_version(self, request):
+        """Return the version of the request for this server."""
+        try:
+            version = request.get_parameter('oauth_version')
+        except:
+            version = OAUTH_VERSION
+
+        return version
+
+    def _get_signature_method(self, request):
+        """Figure out the signature with some defaults."""
+        try:
+            signature_method = request.get_parameter('oauth_signature_method')
+        except:
+            signature_method = SIGNATURE_METHOD
+
+        try:
+            # Get the signature method object.
+            signature_method = self.signature_methods[signature_method]
+        except:
+            signature_method_names = ', '.join(self.signature_methods.keys())
+            raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names))
+
+        return signature_method
+
+    def _get_verifier(self, request):
+        return request.get_parameter('oauth_verifier')
+
+    def _check_signature(self, request, consumer, token):
+        timestamp, nonce = request._get_timestamp_nonce()
+        self._check_timestamp(timestamp)
+        signature_method = self._get_signature_method(request)
+
+        try:
+            signature = request.get_parameter('oauth_signature')
+        except:
+            raise MissingSignature('Missing oauth_signature.')
+
+        # Validate the signature.
+        valid = signature_method.check(request, consumer, token, signature)
+
+        if not valid:
+            key, base = signature_method.signing_base(request, consumer, token)
+
+            raise Error('Invalid signature. Expected signature base ' 
+                'string: %s' % base)
+
+    def _check_timestamp(self, timestamp):
+        """Verify that timestamp is recentish."""
+        timestamp = int(timestamp)
+        now = int(time.time())
+        lapsed = now - timestamp
+        if lapsed > self.timestamp_threshold:
+            raise Error('Expired timestamp: given %d and now %s has a '
+                'greater difference than threshold %d' % (timestamp, now, 
+                    self.timestamp_threshold))
+
+
+class SignatureMethod(object):
+    """A way of signing requests.
+ 
+    The OAuth protocol lets consumers and service providers pick a way to sign
+    requests. This interface shows the methods expected by the other `oauth`
+    modules for signing requests. Subclass it and implement its methods to
+    provide a new way to sign requests.
+    """
+
+    def signing_base(self, request, consumer, token):
+        """Calculates the string that needs to be signed.
+
+        This method returns a 2-tuple containing the starting key for the
+        signing and the message to be signed. The latter may be used in error
+        messages to help clients debug their software.
+
+        """
+        raise NotImplementedError
+
+    def sign(self, request, consumer, token):
+        """Returns the signature for the given request, based on the consumer
+        and token also provided.
+
+        You should use your implementation of `signing_base()` to build the
+        message to sign. Otherwise it may be less useful for debugging.
+
+        """
+        raise NotImplementedError
+
+    def check(self, request, consumer, token, signature):
+        """Returns whether the given signature is the correct signature for
+        the given consumer and token signing the given request."""
+        built = self.sign(request, consumer, token)
+        return built == signature
+
+
+class SignatureMethod_HMAC_SHA1(SignatureMethod):
+    name = 'HMAC-SHA1'
+
+    def signing_base(self, request, consumer, token):
+        if not hasattr(request, 'normalized_url') or request.normalized_url is None:
+            raise ValueError("Base URL for request is not set.")
+
+        sig = (
+            escape(request.method),
+            escape(request.normalized_url),
+            escape(request.get_normalized_parameters()),
+        )
+
+        key = '%s&' % escape(consumer.secret)
+        if token:
+            key += escape(token.secret)
+        raw = '&'.join(sig)
+        return key, raw
+
+    def sign(self, request, consumer, token):
+        """Builds the base signature string."""
+        key, raw = self.signing_base(request, consumer, token)
+
+        hashed = hmac.new(key, raw, sha)
+
+        # Calculate the digest base 64.
+        return binascii.b2a_base64(hashed.digest())[:-1]
+
+
+class SignatureMethod_PLAINTEXT(SignatureMethod):
+
+    name = 'PLAINTEXT'
+
+    def signing_base(self, request, consumer, token):
+        """Concatenates the consumer key and secret with the token's
+        secret."""
+        sig = '%s&' % escape(consumer.secret)
+        if token:
+            sig = sig + escape(token.secret)
+        return sig, sig
+
+    def sign(self, request, consumer, token):
+        key, raw = self.signing_base(request, consumer, token)
+        return raw

+ 18 - 0
desktop/core/ext-py/python-oauth2/oauth2/_version.py

@@ -0,0 +1,18 @@
+# This is the version of this source code.
+
+manual_verstr = "1.5"
+
+
+
+auto_build_num = "211"
+
+
+
+verstr = manual_verstr + "." + auto_build_num
+try:
+    from pyutil.version_class import Version as pyutil_Version
+    __version__ = pyutil_Version(verstr)
+except (ImportError, ValueError):
+    # Maybe there is no pyutil installed.
+    from distutils.version import LooseVersion as distutils_Version
+    __version__ = distutils_Version(verstr)

+ 0 - 0
desktop/core/ext-py/python-oauth2/oauth2/clients/__init__.py


+ 40 - 0
desktop/core/ext-py/python-oauth2/oauth2/clients/imap.py

@@ -0,0 +1,40 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import imaplib
+
+
+class IMAP4_SSL(imaplib.IMAP4_SSL):
+    """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH."""
+
+    def authenticate(self, url, consumer, token):
+        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, oauth2.Token):
+            raise ValueError("Invalid token.")
+
+        imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH',
+            lambda x: oauth2.build_xoauth_string(url, consumer, token))

+ 41 - 0
desktop/core/ext-py/python-oauth2/oauth2/clients/smtp.py

@@ -0,0 +1,41 @@
+"""
+The MIT License
+
+Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+
+import oauth2
+import smtplib
+import base64
+
+
+class SMTP(smtplib.SMTP):
+    """SMTP wrapper for smtplib.SMTP that implements XOAUTH."""
+
+    def authenticate(self, url, consumer, token):
+        if consumer is not None and not isinstance(consumer, oauth2.Consumer):
+            raise ValueError("Invalid consumer.")
+
+        if token is not None and not isinstance(token, oauth2.Token):
+            raise ValueError("Invalid token.")
+
+        self.docmd('AUTH', 'XOAUTH %s' % \
+            base64.b64encode(oauth2.build_xoauth_string(url, consumer, token)))

+ 0 - 0
desktop/core/ext-py/python-oauth2/setup.cfg


+ 40 - 0
desktop/core/ext-py/python-oauth2/setup.py

@@ -0,0 +1,40 @@
+#!/usr/bin/env python
+from setuptools import setup, find_packages
+import os, re
+
+PKG='oauth2'
+VERSIONFILE = os.path.join('oauth2', '_version.py')
+verstr = "unknown"
+try:
+    verstrline = open(VERSIONFILE, "rt").read()
+except EnvironmentError:
+    pass # Okay, there is no version file.
+else:
+    MVSRE = r"^manual_verstr *= *['\"]([^'\"]*)['\"]"
+    mo = re.search(MVSRE, verstrline, re.M)
+    if mo:
+        mverstr = mo.group(1)
+    else:
+        print "unable to find version in %s" % (VERSIONFILE,)
+        raise RuntimeError("if %s.py exists, it must be well-formed" % (VERSIONFILE,))
+    AVSRE = r"^auto_build_num *= *['\"]([^'\"]*)['\"]"
+    mo = re.search(AVSRE, verstrline, re.M)
+    if mo:
+        averstr = mo.group(1)
+    else:
+        averstr = ''
+    verstr = '.'.join([mverstr, averstr])
+
+setup(name=PKG,
+      version=verstr,
+      description="library for OAuth version 1.0",
+      author="Joe Stump",
+      author_email="joe@simplegeo.com",
+      url="http://github.com/simplegeo/python-oauth2",
+      packages = find_packages(),
+      install_requires = ['httplib2'],
+      license = "MIT License",
+      keywords="oauth",
+      zip_safe = True,
+      test_suite="tests",
+      tests_require=['coverage', 'mock'])

+ 0 - 0
desktop/core/ext-py/python-oauth2/tests/__init__.py


+ 1309 - 0
desktop/core/ext-py/python-oauth2/tests/test_oauth.py

@@ -0,0 +1,1309 @@
+# -*- coding: utf-8 -*-
+
+"""
+The MIT License
+
+Copyright (c) 2009 Vic Fryzel
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+"""
+import sys
+import os
+import unittest
+import oauth2 as oauth
+import random
+import time
+import urllib
+import urlparse
+from types import ListType
+import mock
+import httplib2
+
+# Fix for python2.5 compatibility
+try:
+    from urlparse import parse_qs, parse_qsl
+except ImportError:
+    from cgi import parse_qs, parse_qsl
+
+
+sys.path[0:0] = [os.path.join(os.path.dirname(__file__), ".."),]
+
+
+class TestError(unittest.TestCase):
+    def test_message(self):
+        try:
+            raise oauth.Error
+        except oauth.Error, e:
+            self.assertEqual(e.message, 'OAuth error occurred.')
+
+        msg = 'OMG THINGS BROKE!!!!'
+        try:
+            raise oauth.Error(msg)
+        except oauth.Error, e:
+            self.assertEqual(e.message, msg)
+
+    def test_str(self):
+        try:
+            raise oauth.Error
+        except oauth.Error, e:
+            self.assertEquals(str(e), 'OAuth error occurred.')
+
+class TestGenerateFunctions(unittest.TestCase):
+    def test_build_auth_header(self):
+        header = oauth.build_authenticate_header()
+        self.assertEqual(header['WWW-Authenticate'], 'OAuth realm=""')
+        self.assertEqual(len(header), 1)
+        realm = 'http://example.myrealm.com/'
+        header = oauth.build_authenticate_header(realm)
+        self.assertEqual(header['WWW-Authenticate'], 'OAuth realm="%s"' %
+                         realm)
+        self.assertEqual(len(header), 1)
+
+    def test_build_xoauth_string(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
+        token = oauth.Token('user_token', 'user_secret')
+        url = "https://mail.google.com/mail/b/joe@example.com/imap/"
+        xoauth_string = oauth.build_xoauth_string(url, consumer, token)
+
+        method, oauth_url, oauth_string = xoauth_string.split(' ')
+
+        self.assertEqual("GET", method)
+        self.assertEqual(url, oauth_url)
+
+        returned = {}
+        parts = oauth_string.split(',')
+        for part in parts:
+            var, val = part.split('=')
+            returned[var] = val.strip('"') 
+
+        self.assertEquals('HMAC-SHA1', returned['oauth_signature_method'])
+        self.assertEquals('user_token', returned['oauth_token'])
+        self.assertEquals('consumer_token', returned['oauth_consumer_key'])
+        self.assertTrue('oauth_signature' in returned, 'oauth_signature')
+
+    def test_escape(self):
+        string = 'http://whatever.com/~someuser/?test=test&other=other'
+        self.assert_('~' in oauth.escape(string))
+        string = '../../../../../../../etc/passwd'
+        self.assert_('../' not in oauth.escape(string))
+
+    def test_gen_nonce(self):
+        nonce = oauth.generate_nonce()
+        self.assertEqual(len(nonce), 8)
+        nonce = oauth.generate_nonce(20)
+        self.assertEqual(len(nonce), 20)
+
+    def test_gen_verifier(self):
+        verifier = oauth.generate_verifier()
+        self.assertEqual(len(verifier), 8)
+        verifier = oauth.generate_verifier(16)
+        self.assertEqual(len(verifier), 16)
+
+    def test_gen_timestamp(self):
+        exp = int(time.time())
+        now = oauth.generate_timestamp()
+        self.assertEqual(exp, now)
+
+class TestConsumer(unittest.TestCase):
+    def setUp(self):
+        self.key = 'my-key'
+        self.secret = 'my-secret'
+        self.consumer = oauth.Consumer(key=self.key, secret=self.secret)
+
+    def test_init(self):
+        self.assertEqual(self.consumer.key, self.key)
+        self.assertEqual(self.consumer.secret, self.secret)
+
+    def test_basic(self):
+        self.assertRaises(ValueError, lambda: oauth.Consumer(None, None))
+        self.assertRaises(ValueError, lambda: oauth.Consumer('asf', None))
+        self.assertRaises(ValueError, lambda: oauth.Consumer(None, 'dasf'))
+
+    def test_str(self):
+        res = dict(parse_qsl(str(self.consumer)))
+        self.assertTrue('oauth_consumer_key' in res)
+        self.assertTrue('oauth_consumer_secret' in res)
+        self.assertEquals(res['oauth_consumer_key'], self.consumer.key)
+        self.assertEquals(res['oauth_consumer_secret'], self.consumer.secret)
+
+class TestToken(unittest.TestCase):
+    def setUp(self):
+        self.key = 'my-key'
+        self.secret = 'my-secret'
+        self.token = oauth.Token(self.key, self.secret)
+
+    def test_basic(self):
+        self.assertRaises(ValueError, lambda: oauth.Token(None, None))
+        self.assertRaises(ValueError, lambda: oauth.Token('asf', None))
+        self.assertRaises(ValueError, lambda: oauth.Token(None, 'dasf'))
+
+    def test_init(self):
+        self.assertEqual(self.token.key, self.key)
+        self.assertEqual(self.token.secret, self.secret)
+        self.assertEqual(self.token.callback, None)
+        self.assertEqual(self.token.callback_confirmed, None)
+        self.assertEqual(self.token.verifier, None)
+
+    def test_set_callback(self):
+        self.assertEqual(self.token.callback, None)
+        self.assertEqual(self.token.callback_confirmed, None)
+        cb = 'http://www.example.com/my-callback'
+        self.token.set_callback(cb)
+        self.assertEqual(self.token.callback, cb)
+        self.assertEqual(self.token.callback_confirmed, 'true')
+        self.token.set_callback(None)
+        self.assertEqual(self.token.callback, None)
+        # TODO: The following test should probably not pass, but it does
+        #       To fix this, check for None and unset 'true' in set_callback
+        #       Additionally, should a confirmation truly be done of the callback?
+        self.assertEqual(self.token.callback_confirmed, 'true')
+
+    def test_set_verifier(self):
+        self.assertEqual(self.token.verifier, None)
+        v = oauth.generate_verifier()
+        self.token.set_verifier(v)
+        self.assertEqual(self.token.verifier, v)
+        self.token.set_verifier()
+        self.assertNotEqual(self.token.verifier, v)
+        self.token.set_verifier('')
+        self.assertEqual(self.token.verifier, '')
+
+    def test_get_callback_url(self):
+        self.assertEqual(self.token.get_callback_url(), None)
+
+        self.token.set_verifier()
+        self.assertEqual(self.token.get_callback_url(), None)
+
+        cb = 'http://www.example.com/my-callback?save=1&return=true'
+        v = oauth.generate_verifier()
+        self.token.set_callback(cb)
+        self.token.set_verifier(v)
+        url = self.token.get_callback_url()
+        verifier_str = '&oauth_verifier=%s' % v
+        self.assertEqual(url, '%s%s' % (cb, verifier_str))
+
+        cb = 'http://www.example.com/my-callback-no-query'
+        v = oauth.generate_verifier()
+        self.token.set_callback(cb)
+        self.token.set_verifier(v)
+        url = self.token.get_callback_url()
+        verifier_str = '?oauth_verifier=%s' % v
+        self.assertEqual(url, '%s%s' % (cb, verifier_str))
+
+    def test_to_string(self):
+        string = 'oauth_token_secret=%s&oauth_token=%s' % (self.secret,
+                                                           self.key)
+        self.assertEqual(self.token.to_string(), string)
+
+        self.token.set_callback('http://www.example.com/my-callback')
+        string += '&oauth_callback_confirmed=true'
+        self.assertEqual(self.token.to_string(), string)
+
+    def _compare_tokens(self, new):
+        self.assertEqual(self.token.key, new.key)
+        self.assertEqual(self.token.secret, new.secret)
+        # TODO: What about copying the callback to the new token?
+        # self.assertEqual(self.token.callback, new.callback)
+        self.assertEqual(self.token.callback_confirmed,
+                         new.callback_confirmed)
+        # TODO: What about copying the verifier to the new token?
+        # self.assertEqual(self.token.verifier, new.verifier)
+
+    def test_to_string(self):
+        tok = oauth.Token('tooken', 'seecret')
+        self.assertEqual(str(tok), 'oauth_token_secret=seecret&oauth_token=tooken')
+
+    def test_from_string(self):
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string(''))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('blahblahblah'))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('blah=blah'))
+
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token_secret=asfdasf'))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token_secret='))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=asfdasf'))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token='))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=&oauth_token_secret='))
+        self.assertRaises(ValueError, lambda: oauth.Token.from_string('oauth_token=tooken%26oauth_token_secret=seecret'))
+
+        string = self.token.to_string()
+        new = oauth.Token.from_string(string)
+        self._compare_tokens(new)
+
+        self.token.set_callback('http://www.example.com/my-callback')
+        string = self.token.to_string()
+        new = oauth.Token.from_string(string)
+        self._compare_tokens(new)
+
+class ReallyEqualMixin:
+    def failUnlessReallyEqual(self, a, b, msg=None):
+        self.failUnlessEqual(a, b, msg=msg)
+        self.failUnlessEqual(type(a), type(b), msg="a :: %r, b :: %r, %r" % (a, b, msg))
+
+class TestFuncs(unittest.TestCase):
+    def test_to_unicode(self):
+        self.failUnlessRaises(TypeError, oauth.to_unicode, '\xae')
+        self.failUnlessRaises(TypeError, oauth.to_unicode_optional_iterator, '\xae')
+        self.failUnlessRaises(TypeError, oauth.to_unicode_optional_iterator, ['\xae'])
+
+        self.failUnlessEqual(oauth.to_unicode(':-)'), u':-)')
+        self.failUnlessEqual(oauth.to_unicode(u'\u00ae'), u'\u00ae')
+        self.failUnlessEqual(oauth.to_unicode('\xc2\xae'), u'\u00ae')
+        self.failUnlessEqual(oauth.to_unicode_optional_iterator([':-)']), [u':-)'])
+        self.failUnlessEqual(oauth.to_unicode_optional_iterator([u'\u00ae']), [u'\u00ae'])
+
+class TestRequest(unittest.TestCase, ReallyEqualMixin):
+    def test_setter(self):
+        url = "http://example.com"
+        method = "GET"
+        req = oauth.Request(method)
+        self.assertTrue(not hasattr(req, 'url') or req.url is None)
+        self.assertTrue(not hasattr(req, 'normalized_url') or req.normalized_url is None)
+
+    def test_deleter(self):
+        url = "http://example.com"
+        method = "GET"
+        req = oauth.Request(method, url)
+
+        try:
+            del req.url
+            url = req.url
+            self.fail("AttributeError should have been raised on empty url.")
+        except AttributeError:
+            pass
+        except Exception, e:
+            self.fail(str(e))
+
+    def test_url(self):
+        url1 = "http://example.com:80/foo.php"
+        url2 = "https://example.com:443/foo.php"
+        exp1 = "http://example.com/foo.php"
+        exp2 = "https://example.com/foo.php"
+        method = "GET"
+
+        req = oauth.Request(method, url1)
+        self.assertEquals(req.normalized_url, exp1)
+        self.assertEquals(req.url, url1)
+
+        req = oauth.Request(method, url2)
+        self.assertEquals(req.normalized_url, exp2)
+        self.assertEquals(req.url, url2)
+
+    def test_bad_url(self):
+        request = oauth.Request()
+        try:
+            request.url = "ftp://example.com"
+            self.fail("Invalid URL scheme was accepted.")
+        except ValueError:
+            pass
+
+    def test_unset_consumer_and_token(self):
+        consumer = oauth.Consumer('my_consumer_key', 'my_consumer_secret')
+        token = oauth.Token('my_key', 'my_secret')
+        request = oauth.Request("GET", "http://example.com/fetch.php")
+        request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer,
+            token)
+
+        self.assertEquals(consumer.key, request['oauth_consumer_key'])
+        self.assertEquals(token.key, request['oauth_token'])
+
+    def test_no_url_set(self):
+        consumer = oauth.Consumer('my_consumer_key', 'my_consumer_secret')
+        token = oauth.Token('my_key', 'my_secret')
+        request = oauth.Request()
+
+        try:
+            try:
+                request.sign_request(oauth.SignatureMethod_HMAC_SHA1(), 
+                    consumer, token)
+            except TypeError:
+                self.fail("Signature method didn't check for a normalized URL.")
+        except ValueError:
+            pass
+
+    def test_url_query(self):
+        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
+        normalized_url = urlparse.urlunparse(urlparse.urlparse(url)[:3] + (None, None, None))
+        method = "GET"
+        
+        req = oauth.Request(method, url)
+        self.assertEquals(req.url, url)
+        self.assertEquals(req.normalized_url, normalized_url)
+
+    def test_get_parameter(self):
+        url = "http://example.com"
+        method = "GET"
+        params = {'oauth_consumer' : 'asdf'}
+        req = oauth.Request(method, url, parameters=params)
+
+        self.assertEquals(req.get_parameter('oauth_consumer'), 'asdf')
+        self.assertRaises(oauth.Error, req.get_parameter, 'blah')
+
+    def test_get_nonoauth_parameters(self):
+
+        oauth_params = {
+            'oauth_consumer': 'asdfasdfasdf'
+        }
+
+        other_params = {
+            u'foo': u'baz',
+            u'bar': u'foo',
+            u'multi': [u'FOO',u'BAR'],
+            u'uni_utf8': u'\xae',
+            u'uni_unicode': u'\u00ae',
+            u'uni_unicode_2': u'åÅøØ',
+        }
+
+        params = oauth_params
+        params.update(other_params)
+
+        req = oauth.Request("GET", "http://example.com", params)
+        self.assertEquals(other_params, req.get_nonoauth_parameters())
+
+    def test_to_header(self):
+        realm = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", realm, params)
+        header, value = req.to_header(realm).items()[0]
+
+        parts = value.split('OAuth ')
+        vars = parts[1].split(', ')
+        self.assertTrue(len(vars), (len(params) + 1))
+
+        res = {}
+        for v in vars:
+            var, val = v.split('=')
+            res[var] = urllib.unquote(val.strip('"'))
+
+        self.assertEquals(realm, res['realm'])
+        del res['realm']
+
+        self.assertTrue(len(res), len(params))
+
+        for key, val in res.items():
+            self.assertEquals(val, params.get(key))
+
+    def test_to_postdata_nonascii(self):
+        realm = "http://sp.example.com/"
+
+        params = {
+            'nonasciithing': u'q\xbfu\xe9 ,aasp u?..a.s',
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", realm, params)
+
+        self.failUnlessReallyEqual(req.to_postdata(), 'nonasciithing=q%C2%BFu%C3%A9%20%2Caasp%20u%3F..a.s&oauth_nonce=4572616e48616d6d65724c61686176&oauth_timestamp=137131200&oauth_consumer_key=0685bd9184jfhq22&oauth_signature_method=HMAC-SHA1&oauth_version=1.0&oauth_token=ad180jjd733klru7&oauth_signature=wOJIO9A2W5mFwDgiDvZbTSMK%252FPY%253D')
+
+    def test_to_postdata(self):
+        realm = "http://sp.example.com/"
+
+        params = {
+            'multi': ['FOO','BAR'],
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", realm, params)
+
+        flat = [('multi','FOO'),('multi','BAR')]
+        del params['multi']
+        flat.extend(params.items())
+        kf = lambda x: x[0]
+        self.assertEquals(sorted(flat, key=kf), sorted(parse_qsl(req.to_postdata()), key=kf))
+
+    def test_to_url(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        exp = urlparse.urlparse("%s?%s" % (url, urllib.urlencode(params)))
+        res = urlparse.urlparse(req.to_url())
+        self.assertEquals(exp.scheme, res.scheme)
+        self.assertEquals(exp.netloc, res.netloc)
+        self.assertEquals(exp.path, res.path)
+
+        a = parse_qs(exp.query)
+        b = parse_qs(res.query)
+        self.assertEquals(a, b)
+    
+    def test_to_url_with_query(self):
+        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        # Note: the url above already has query parameters, so append new ones with &
+        exp = urlparse.urlparse("%s&%s" % (url, urllib.urlencode(params)))
+        res = urlparse.urlparse(req.to_url())
+        self.assertEquals(exp.scheme, res.scheme)
+        self.assertEquals(exp.netloc, res.netloc)
+        self.assertEquals(exp.path, res.path)
+
+        a = parse_qs(exp.query)
+        b = parse_qs(res.query)
+        self.assertTrue('alt' in b)
+        self.assertTrue('max-contacts' in b)
+        self.assertEquals(b['alt'], ['json'])
+        self.assertEquals(b['max-contacts'], ['10'])
+        self.assertEquals(a, b)
+
+    def test_signature_base_string_nonascii_nonutf8(self):
+        consumer = oauth.Consumer('consumer_token', 'consumer_secret')
+
+        url = u'http://api.simplegeo.com:80/1.0/places/address.json?q=monkeys&category=animal&address=41+Decatur+St,+San+Francisc\u2766,+CA'
+        req = oauth.Request("GET", url)
+        self.failUnlessReallyEqual(req.normalized_url, u'http://api.simplegeo.com/1.0/places/address.json')
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+        url = 'http://api.simplegeo.com:80/1.0/places/address.json?q=monkeys&category=animal&address=41+Decatur+St,+San+Francisc\xe2\x9d\xa6,+CA'
+        req = oauth.Request("GET", url)
+        self.failUnlessReallyEqual(req.normalized_url, u'http://api.simplegeo.com/1.0/places/address.json')
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+        url = 'http://api.simplegeo.com:80/1.0/places/address.json?q=monkeys&category=animal&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA'
+        req = oauth.Request("GET", url)
+        self.failUnlessReallyEqual(req.normalized_url, u'http://api.simplegeo.com/1.0/places/address.json')
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+        url = u'http://api.simplegeo.com:80/1.0/places/address.json?q=monkeys&category=animal&address=41+Decatur+St,+San+Francisc%E2%9D%A6,+CA'
+        req = oauth.Request("GET", url)
+        self.failUnlessReallyEqual(req.normalized_url, u'http://api.simplegeo.com/1.0/places/address.json')
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), consumer, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'WhufgeZKyYpKsI70GZaiDaYwl6g=')
+
+    def test_signature_base_string_with_query(self):
+        url = "https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10"
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+        req = oauth.Request("GET", url, params)
+        self.assertEquals(req.normalized_url, 'https://www.google.com/m8/feeds/contacts/default/full/')
+        self.assertEquals(req.url, 'https://www.google.com/m8/feeds/contacts/default/full/?alt=json&max-contacts=10')
+        normalized_params = parse_qsl(req.get_normalized_parameters())
+        self.assertTrue(len(normalized_params), len(params) + 2)
+        normalized_params = dict(normalized_params)
+        for key, value in params.iteritems():
+            if key == 'oauth_signature':
+                continue
+            self.assertEquals(value, normalized_params[key])
+        self.assertEquals(normalized_params['alt'], 'json')
+        self.assertEquals(normalized_params['max-contacts'], '10')
+
+    def test_get_normalized_parameters_empty(self):
+        url = "http://sp.example.com/?empty="
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected='empty='
+
+        self.assertEquals(expected, res)
+
+    def test_get_normalized_parameters_duplicate(self):
+        url = "http://example.com/v2/search/videos?oauth_nonce=79815175&oauth_timestamp=1295397962&oauth_consumer_key=mykey&oauth_signature_method=HMAC-SHA1&q=car&oauth_version=1.0&offset=10&oauth_signature=spWLI%2FGQjid7sQVd5%2FarahRxzJg%3D"
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected='oauth_consumer_key=mykey&oauth_nonce=79815175&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1295397962&oauth_version=1.0&offset=10&q=car'
+
+        self.assertEquals(expected, res)
+
+    def test_get_normalized_parameters_from_url(self):
+        # example copied from
+        # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js
+        # which in turns says that it was copied from
+        # http://oauth.net/core/1.0/#sig_base_example .
+        url = "http://photos.example.net/photos?file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original"
+
+        req = oauth.Request("GET", url)
+
+        res = req.get_normalized_parameters()
+
+        expected = 'file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original'
+
+        self.assertEquals(expected, res)
+
+    def test_signing_base(self):
+        # example copied from
+        # https://github.com/ciaranj/node-oauth/blob/master/tests/oauth.js
+        # which in turns says that it was copied from
+        # http://oauth.net/core/1.0/#sig_base_example .
+        url = "http://photos.example.net/photos?file=vacation.jpg&oauth_consumer_key=dpf43f3p2l4k3l03&oauth_nonce=kllo9940pd9333jh&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1191242096&oauth_token=nnch734d00sl2jdk&oauth_version=1.0&size=original"
+
+        req = oauth.Request("GET", url)
+
+        sm = oauth.SignatureMethod_HMAC_SHA1()
+
+        consumer = oauth.Consumer('dpf43f3p2l4k3l03', 'foo')
+        key, raw = sm.signing_base(req, consumer, None)
+
+        expected = 'GET&http%3A%2F%2Fphotos.example.net%2Fphotos&file%3Dvacation.jpg%26oauth_consumer_key%3Ddpf43f3p2l4k3l03%26oauth_nonce%3Dkllo9940pd9333jh%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1191242096%26oauth_token%3Dnnch734d00sl2jdk%26oauth_version%3D1.0%26size%3Doriginal'
+        self.assertEquals(expected, raw)
+
+    def test_get_normalized_parameters(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'multi': ['FOO','BAR', u'\u00ae', '\xc2\xae'],
+            'multi_same': ['FOO','FOO'],
+            'uni_utf8_bytes': '\xc2\xae',
+            'uni_unicode_object': u'\u00ae'
+        }
+
+        req = oauth.Request("GET", url, params)
+
+        res = req.get_normalized_parameters()
+
+        expected='multi=BAR&multi=FOO&multi=%C2%AE&multi=%C2%AE&multi_same=FOO&multi_same=FOO&oauth_consumer_key=0685bd9184jfhq22&oauth_nonce=4572616e48616d6d65724c61686176&oauth_signature_method=HMAC-SHA1&oauth_timestamp=137131200&oauth_token=ad180jjd733klru7&oauth_version=1.0&uni_unicode_object=%C2%AE&uni_utf8_bytes=%C2%AE'
+
+        self.assertEquals(expected, res)
+
+    def test_get_normalized_parameters_ignores_auth_signature(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_signature': "some-random-signature-%d" % random.randint(1000, 2000),
+            'oauth_token': "ad180jjd733klru7",
+        }
+
+        req = oauth.Request("GET", url, params)
+
+        res = req.get_normalized_parameters()
+
+        self.assertNotEquals(urllib.urlencode(sorted(params.items())), res)
+
+        foo = params.copy()
+        del foo["oauth_signature"]
+        self.assertEqual(urllib.urlencode(sorted(foo.items())), res)
+
+    def test_set_signature_method(self):
+        consumer = oauth.Consumer('key', 'secret')
+        client = oauth.Client(consumer)
+
+        class Blah:
+            pass
+
+        try:
+            client.set_signature_method(Blah())
+            self.fail("Client.set_signature_method() accepted invalid method.")
+        except ValueError:
+            pass
+
+        m = oauth.SignatureMethod_HMAC_SHA1()
+        client.set_signature_method(m)
+        self.assertEquals(m, client.method)
+
+    def test_get_normalized_string_escapes_spaces_properly(self):
+        url = "http://sp.example.com/"
+        params = {
+            "some_random_data": random.randint(100, 1000),
+            "data": "This data with a random number (%d) has spaces!" % random.randint(1000, 2000),
+        }
+
+        req = oauth.Request("GET", url, params)
+        res = req.get_normalized_parameters()
+        expected = urllib.urlencode(sorted(params.items())).replace('+', '%20')
+        self.assertEqual(expected, res)
+
+    @mock.patch('oauth2.Request.make_timestamp')
+    @mock.patch('oauth2.Request.make_nonce')
+    def test_request_nonutf8_bytes(self, mock_make_nonce, mock_make_timestamp):
+        mock_make_nonce.return_value = 5
+        mock_make_timestamp.return_value = 6
+
+        tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+        con = oauth.Consumer(key="con-test-key", secret="con-test-secret")
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_token': tok.key,
+            'oauth_consumer_key': con.key
+        }
+
+        # If someone passes a sequence of bytes which is not ascii for
+        # url, we'll raise an exception as early as possible.
+        url = "http://sp.example.com/\x92" # It's actually cp1252-encoding...
+        self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+        # And if they pass an unicode, then we'll use it.
+        url = u'http://sp.example.com/\u2019'
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'cMzvCkhvLL57+sTIxLITTHfkqZk=')
+
+        # And if it is a utf-8-encoded-then-percent-encoded non-ascii
+        # thing, we'll decode it and use it.
+        url = "http://sp.example.com/%E2%80%99"
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'yMLKOyNKC/DkyhUOb8DLSvceEWE=')
+
+        # Same thing with the params.
+        url = "http://sp.example.com/"
+
+        # If someone passes a sequence of bytes which is not ascii in
+        # params, we'll raise an exception as early as possible.
+        params['non_oauth_thing'] = '\xae', # It's actually cp1252-encoding...
+        self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+        # And if they pass a unicode, then we'll use it.
+        params['non_oauth_thing'] = u'\u2019'
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], '0GU50m0v60CVDB5JnoBXnvvvKx4=')
+
+        # And if it is a utf-8-encoded non-ascii thing, we'll decode
+        # it and use it.
+        params['non_oauth_thing'] = '\xc2\xae'
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_signature'], 'pqOCu4qvRTiGiXB8Z61Jsey0pMM=')
+
+
+        # Also if there are non-utf8 bytes in the query args.
+        url = "http://sp.example.com/?q=\x92" # cp1252
+        self.assertRaises(TypeError, oauth.Request, method="GET", url=url, parameters=params)
+
+    def test_request_hash_of_body(self):
+        tok = oauth.Token(key="token", secret="tok-test-secret")
+        con = oauth.Consumer(key="consumer", secret="con-test-secret")
+
+        # Example 1a from Appendix A.1 of
+        # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html
+        # Except that we get a differetn result than they do.
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 10288510250934,
+            'oauth_timestamp': 1236874155,
+            'oauth_consumer_key': con.key
+        }
+
+        url = u"http://www.example.com/resource"
+        req = oauth.Request(method="PUT", url=url, parameters=params, body="Hello World!", is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_body_hash'], 'Lve95gjOVATpfV8EL5X4nxwjKHE=')
+        self.failUnlessReallyEqual(req['oauth_signature'], 't+MX8l/0S8hdbVQL99nD0X1fPnM=')
+        # oauth-bodyhash.html A.1 has
+        # '08bUFF%2Fjmp59mWB7cSgCYBUpJ0U%3D', but I don't see how that
+        # is possible.
+
+        # Example 1b
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 10369470270925,
+            'oauth_timestamp': 1236874236,
+            'oauth_consumer_key': con.key
+        }
+
+        req = oauth.Request(method="PUT", url=url, parameters=params, body="Hello World!", is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_body_hash'], 'Lve95gjOVATpfV8EL5X4nxwjKHE=')
+        self.failUnlessReallyEqual(req['oauth_signature'], 'CTFmrqJIGT7NsWJ42OrujahTtTc=')
+
+        # Appendix A.2
+        params = {
+            'oauth_version': "1.0",
+            'oauth_token': tok.key,
+            'oauth_nonce': 8628868109991,
+            'oauth_timestamp': 1238395022,
+            'oauth_consumer_key': con.key
+        }
+
+        req = oauth.Request(method="GET", url=url, parameters=params, is_form_encoded=False)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, None)
+        self.failUnlessReallyEqual(req['oauth_body_hash'], '2jmj7l5rSw0yVb/vlWAYkK/YBwk=')
+        self.failUnlessReallyEqual(req['oauth_signature'], 'Zhl++aWSP0O3/hYQ0CuBc7jv38I=')
+
+
+    def test_sign_request(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200"
+        }
+
+        tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+        con = oauth.Consumer(key="con-test-key", secret="con-test-secret")
+
+        params['oauth_token'] = tok.key
+        params['oauth_consumer_key'] = con.key
+        req = oauth.Request(method="GET", url=url, parameters=params)
+
+        methods = {
+            'DX01TdHws7OninCLK9VztNTH1M4=': oauth.SignatureMethod_HMAC_SHA1(),
+            'con-test-secret&tok-test-secret': oauth.SignatureMethod_PLAINTEXT()
+            }
+
+        for exp, method in methods.items():
+            req.sign_request(method, con, tok)
+            self.assertEquals(req['oauth_signature_method'], method.name)
+            self.assertEquals(req['oauth_signature'], exp)
+
+        # Also if there are non-ascii chars in the URL.
+        url = "http://sp.example.com/\xe2\x80\x99" # utf-8 bytes
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEquals(req['oauth_signature'], 'loFvp5xC7YbOgd9exIO6TxB7H4s=')
+
+        url = u'http://sp.example.com/\u2019' # Python unicode object
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEquals(req['oauth_signature'], 'loFvp5xC7YbOgd9exIO6TxB7H4s=')
+
+        # Also if there are non-ascii chars in the query args.
+        url = "http://sp.example.com/?q=\xe2\x80\x99" # utf-8 bytes
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEquals(req['oauth_signature'], 'IBw5mfvoCsDjgpcsVKbyvsDqQaU=')
+
+        url = u'http://sp.example.com/?q=\u2019' # Python unicode object
+        req = oauth.Request(method="GET", url=url, parameters=params)
+        req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), con, tok)
+        self.assertEquals(req['oauth_signature'], 'IBw5mfvoCsDjgpcsVKbyvsDqQaU=')
+
+    def test_from_request(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        req = oauth.Request("GET", url, params)
+        headers = req.to_header()
+
+        # Test from the headers
+        req = oauth.Request.from_request("GET", url, headers)
+        self.assertEquals(req.method, "GET")
+        self.assertEquals(req.url, url)
+
+        self.assertEquals(params, req.copy())
+
+        # Test with bad OAuth headers
+        bad_headers = {
+            'Authorization' : 'OAuth this is a bad header'
+        }
+
+        self.assertRaises(oauth.Error, oauth.Request.from_request, "GET",
+            url, bad_headers)
+
+        # Test getting from query string
+        qs = urllib.urlencode(params)
+        req = oauth.Request.from_request("GET", url, query_string=qs)
+
+        exp = parse_qs(qs, keep_blank_values=False)
+        for k, v in exp.iteritems():
+            exp[k] = urllib.unquote(v[0])
+
+        self.assertEquals(exp, req.copy())
+
+        # Test that a boned from_request() call returns None
+        req = oauth.Request.from_request("GET", url)
+        self.assertEquals(None, req)
+
+    def test_from_token_and_callback(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': "137131200",
+            'oauth_consumer_key': "0685bd9184jfhq22",
+            'oauth_signature_method': "HMAC-SHA1",
+            'oauth_token': "ad180jjd733klru7",
+            'oauth_signature': "wOJIO9A2W5mFwDgiDvZbTSMK%2FPY%3D",
+        }
+
+        tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+        req = oauth.Request.from_token_and_callback(tok)
+        self.assertFalse('oauth_callback' in req)
+        self.assertEquals(req['oauth_token'], tok.key)
+
+        req = oauth.Request.from_token_and_callback(tok, callback=url)
+        self.assertTrue('oauth_callback' in req)
+        self.assertEquals(req['oauth_callback'], url)
+
+    def test_from_consumer_and_token(self):
+        url = "http://sp.example.com/"
+
+        tok = oauth.Token(key="tok-test-key", secret="tok-test-secret")
+        tok.set_verifier('this_is_a_test_verifier')
+        con = oauth.Consumer(key="con-test-key", secret="con-test-secret")
+        req = oauth.Request.from_consumer_and_token(con, token=tok,
+            http_method="GET", http_url=url)
+
+        self.assertEquals(req['oauth_token'], tok.key)
+        self.assertEquals(req['oauth_consumer_key'], con.key)
+        self.assertEquals(tok.verifier, req['oauth_verifier'])
+
+class SignatureMethod_Bad(oauth.SignatureMethod):
+    name = "BAD"
+
+    def signing_base(self, request, consumer, token):
+        return ""
+
+    def sign(self, request, consumer, token):
+        return "invalid-signature"
+
+
+class TestServer(unittest.TestCase):
+    def setUp(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': "1.0",
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': int(time.time()),
+            'bar': 'blerg',
+            'multi': ['FOO','BAR'],
+            'foo': 59
+        }
+
+        self.consumer = oauth.Consumer(key="consumer-key",
+            secret="consumer-secret")
+        self.token = oauth.Token(key="token-key", secret="token-secret")
+
+        params['oauth_token'] = self.token.key
+        params['oauth_consumer_key'] = self.consumer.key
+        self.request = oauth.Request(method="GET", url=url, parameters=params)
+
+        signature_method = oauth.SignatureMethod_HMAC_SHA1()
+        self.request.sign_request(signature_method, self.consumer, self.token)
+
+    def test_init(self):
+        server = oauth.Server(signature_methods={'HMAC-SHA1' : oauth.SignatureMethod_HMAC_SHA1()})
+        self.assertTrue('HMAC-SHA1' in server.signature_methods)
+        self.assertTrue(isinstance(server.signature_methods['HMAC-SHA1'],
+            oauth.SignatureMethod_HMAC_SHA1))
+
+        server = oauth.Server()
+        self.assertEquals(server.signature_methods, {})
+
+    def test_add_signature_method(self):
+        server = oauth.Server()
+        res = server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+        self.assertTrue(len(res) == 1)
+        self.assertTrue('HMAC-SHA1' in res)
+        self.assertTrue(isinstance(res['HMAC-SHA1'],
+            oauth.SignatureMethod_HMAC_SHA1))
+
+        res = server.add_signature_method(oauth.SignatureMethod_PLAINTEXT())
+        self.assertTrue(len(res) == 2)
+        self.assertTrue('PLAINTEXT' in res)
+        self.assertTrue(isinstance(res['PLAINTEXT'],
+            oauth.SignatureMethod_PLAINTEXT))
+
+    def test_verify_request(self):
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        parameters = server.verify_request(self.request, self.consumer,
+            self.token)
+
+        self.assertTrue('bar' in parameters)
+        self.assertTrue('foo' in parameters)
+        self.assertTrue('multi' in parameters)
+        self.assertEquals(parameters['bar'], 'blerg')
+        self.assertEquals(parameters['foo'], 59)
+        self.assertEquals(parameters['multi'], ['FOO','BAR'])
+
+    def test_build_authenticate_header(self):
+        server = oauth.Server()
+        headers = server.build_authenticate_header('example.com')
+        self.assertTrue('WWW-Authenticate' in headers)
+        self.assertEquals('OAuth realm="example.com"', 
+            headers['WWW-Authenticate'])
+
+    def test_no_version(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': int(time.time()),
+            'bar': 'blerg',
+            'multi': ['FOO','BAR'],
+            'foo': 59
+        }
+
+        self.consumer = oauth.Consumer(key="consumer-key",
+            secret="consumer-secret")
+        self.token = oauth.Token(key="token-key", secret="token-secret")
+
+        params['oauth_token'] = self.token.key
+        params['oauth_consumer_key'] = self.consumer.key
+        self.request = oauth.Request(method="GET", url=url, parameters=params)
+
+        signature_method = oauth.SignatureMethod_HMAC_SHA1()
+        self.request.sign_request(signature_method, self.consumer, self.token)
+
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        parameters = server.verify_request(self.request, self.consumer,
+            self.token)
+
+    def test_invalid_version(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': '222.9922',
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': int(time.time()),
+            'bar': 'blerg',
+            'multi': ['foo','bar'],
+            'foo': 59
+        }
+
+        consumer = oauth.Consumer(key="consumer-key",
+            secret="consumer-secret")
+        token = oauth.Token(key="token-key", secret="token-secret")
+
+        params['oauth_token'] = token.key
+        params['oauth_consumer_key'] = consumer.key
+        request = oauth.Request(method="GET", url=url, parameters=params)
+
+        signature_method = oauth.SignatureMethod_HMAC_SHA1()
+        request.sign_request(signature_method, consumer, token)
+
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        self.assertRaises(oauth.Error, server.verify_request, request, consumer, token)
+
+    def test_invalid_signature_method(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': '1.0',
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': int(time.time()),
+            'bar': 'blerg',
+            'multi': ['FOO','BAR'],
+            'foo': 59
+        }
+
+        consumer = oauth.Consumer(key="consumer-key",
+            secret="consumer-secret")
+        token = oauth.Token(key="token-key", secret="token-secret")
+
+        params['oauth_token'] = token.key
+        params['oauth_consumer_key'] = consumer.key
+        request = oauth.Request(method="GET", url=url, parameters=params)
+
+        signature_method = SignatureMethod_Bad()
+        request.sign_request(signature_method, consumer, token)
+
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        self.assertRaises(oauth.Error, server.verify_request, request,
+            consumer, token)
+
+    def test_missing_signature(self):
+        url = "http://sp.example.com/"
+
+        params = {
+            'oauth_version': '1.0',
+            'oauth_nonce': "4572616e48616d6d65724c61686176",
+            'oauth_timestamp': int(time.time()),
+            'bar': 'blerg',
+            'multi': ['FOO','BAR'],
+            'foo': 59
+        }
+
+        consumer = oauth.Consumer(key="consumer-key",
+            secret="consumer-secret")
+        token = oauth.Token(key="token-key", secret="token-secret")
+
+        params['oauth_token'] = token.key
+        params['oauth_consumer_key'] = consumer.key
+        request = oauth.Request(method="GET", url=url, parameters=params)
+
+        signature_method = oauth.SignatureMethod_HMAC_SHA1()
+        request.sign_request(signature_method, consumer, token)
+        del request['oauth_signature']
+
+        server = oauth.Server()
+        server.add_signature_method(oauth.SignatureMethod_HMAC_SHA1())
+
+        self.assertRaises(oauth.MissingSignature, server.verify_request,
+            request, consumer, token)
+
+
+# Request Token: http://oauth-sandbox.sevengoslings.net/request_token
+# Auth: http://oauth-sandbox.sevengoslings.net/authorize
+# Access Token: http://oauth-sandbox.sevengoslings.net/access_token
+# Two-legged: http://oauth-sandbox.sevengoslings.net/two_legged
+# Three-legged: http://oauth-sandbox.sevengoslings.net/three_legged
+# Key: bd37aed57e15df53
+# Secret: 0e9e6413a9ef49510a4f68ed02cd
+class TestClient(unittest.TestCase):
+#    oauth_uris = {
+#        'request_token': '/request_token.php',
+#        'access_token': '/access_token.php'
+#    }
+
+    oauth_uris = {
+        'request_token': '/request_token',
+        'authorize': '/authorize',
+        'access_token': '/access_token',
+        'two_legged': '/two_legged',
+        'three_legged': '/three_legged'
+    }
+
+    consumer_key = 'bd37aed57e15df53'
+    consumer_secret = '0e9e6413a9ef49510a4f68ed02cd'
+    host = 'http://oauth-sandbox.sevengoslings.net'
+
+    def setUp(self):
+        self.consumer = oauth.Consumer(key=self.consumer_key,
+            secret=self.consumer_secret)
+
+        self.body = {
+            'foo': 'bar',
+            'bar': 'foo',
+            'multi': ['FOO','BAR'],
+            'blah': 599999
+        }
+
+    def _uri(self, type):
+        uri = self.oauth_uris.get(type)
+        if uri is None:
+            raise KeyError("%s is not a valid OAuth URI type." % type)
+
+        return "%s%s" % (self.host, uri)
+
+    def create_simple_multipart_data(self, data):
+        boundary = '---Boundary-%d' % random.randint(1,1000)
+        crlf = '\r\n'
+        items = []
+        for key, value in data.iteritems():
+            items += [
+                '--'+boundary,
+                'Content-Disposition: form-data; name="%s"'%str(key),
+                '',
+                str(value),
+            ]
+        items += ['', '--'+boundary+'--', '']
+        content_type = 'multipart/form-data; boundary=%s' % boundary
+        return content_type, crlf.join(items)
+
+    def test_init(self):
+        class Blah():
+            pass
+
+        try:
+            client = oauth.Client(Blah())
+            self.fail("Client.__init__() accepted invalid Consumer.")
+        except ValueError:
+            pass
+
+        consumer = oauth.Consumer('token', 'secret')
+        try:
+            client = oauth.Client(consumer, Blah())
+            self.fail("Client.__init__() accepted invalid Token.")
+        except ValueError:
+            pass
+
+    def test_access_token_get(self):
+        """Test getting an access token via GET."""
+        client = oauth.Client(self.consumer, None)
+        resp, content = client.request(self._uri('request_token'), "GET")
+
+        self.assertEquals(int(resp['status']), 200)
+
+    def test_access_token_post(self):
+        """Test getting an access token via POST."""
+        client = oauth.Client(self.consumer, None)
+        resp, content = client.request(self._uri('request_token'), "POST")
+
+        self.assertEquals(int(resp['status']), 200)
+
+        res = dict(parse_qsl(content))
+        self.assertTrue('oauth_token' in res)
+        self.assertTrue('oauth_token_secret' in res)
+
+    def _two_legged(self, method):
+        client = oauth.Client(self.consumer, None)
+
+        return client.request(self._uri('two_legged'), method,
+            body=urllib.urlencode(self.body))
+
+    def test_two_legged_post(self):
+        """A test of a two-legged OAuth POST request."""
+        resp, content = self._two_legged("POST")
+
+        self.assertEquals(int(resp['status']), 200)
+
+    def test_two_legged_get(self):
+        """A test of a two-legged OAuth GET request."""
+        resp, content = self._two_legged("GET")
+        self.assertEquals(int(resp['status']), 200)
+
+    @mock.patch('httplib2.Http.request')
+    def test_multipart_post_does_not_alter_body(self, mockHttpRequest):
+        random_result = random.randint(1,100)
+
+        data = {
+            'rand-%d'%random.randint(1,100):random.randint(1,100),
+        }
+        content_type, body = self.create_simple_multipart_data(data)
+
+        client = oauth.Client(self.consumer, None)
+        uri = self._uri('two_legged')
+
+        def mockrequest(cl, ur, **kw):
+            self.failUnless(cl is client)
+            self.failUnless(ur is uri)
+            self.failUnlessEqual(frozenset(kw.keys()), frozenset(['method', 'body', 'redirections', 'connection_type', 'headers']))
+            self.failUnlessEqual(kw['body'], body)
+            self.failUnlessEqual(kw['connection_type'], None)
+            self.failUnlessEqual(kw['method'], 'POST')
+            self.failUnlessEqual(kw['redirections'], httplib2.DEFAULT_MAX_REDIRECTS)
+            self.failUnless(isinstance(kw['headers'], dict))
+
+            return random_result
+
+        mockHttpRequest.side_effect = mockrequest
+
+        result = client.request(uri, 'POST', headers={'Content-Type':content_type}, body=body)
+        self.assertEqual(result, random_result)
+
+    @mock.patch('httplib2.Http.request')
+    def test_url_with_query_string(self, mockHttpRequest):
+        uri = 'http://example.com/foo/bar/?show=thundercats&character=snarf'
+        client = oauth.Client(self.consumer, None)
+        random_result = random.randint(1,100)
+
+        def mockrequest(cl, ur, **kw):
+            self.failUnless(cl is client)
+            self.failUnlessEqual(frozenset(kw.keys()), frozenset(['method', 'body', 'redirections', 'connection_type', 'headers']))
+            self.failUnlessEqual(kw['body'], '')
+            self.failUnlessEqual(kw['connection_type'], None)
+            self.failUnlessEqual(kw['method'], 'GET')
+            self.failUnlessEqual(kw['redirections'], httplib2.DEFAULT_MAX_REDIRECTS)
+            self.failUnless(isinstance(kw['headers'], dict))
+
+            req = oauth.Request.from_consumer_and_token(self.consumer, None,
+                    http_method='GET', http_url=uri, parameters={})
+            req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), self.consumer, None)
+            expected = parse_qsl(urlparse.urlparse(req.to_url()).query)
+            actual = parse_qsl(urlparse.urlparse(ur).query)
+            self.failUnlessEqual(len(expected), len(actual))
+            actual = dict(actual)
+            for key, value in expected:
+                if key not in ('oauth_signature', 'oauth_nonce', 'oauth_timestamp'):
+                    self.failUnlessEqual(actual[key], value)
+
+            return random_result
+
+        mockHttpRequest.side_effect = mockrequest
+
+        client.request(uri, 'GET')
+
+    @mock.patch('httplib2.Http.request')
+    @mock.patch('oauth2.Request.from_consumer_and_token')
+    def test_multiple_values_for_a_key(self, mockReqConstructor, mockHttpRequest):
+        client = oauth.Client(self.consumer, None)
+
+        request = oauth.Request("GET", "http://example.com/fetch.php", parameters={'multi': ['1', '2']})
+        mockReqConstructor.return_value = request
+
+        client.request('http://whatever', 'POST', body='multi=1&multi=2')
+
+        self.failUnlessEqual(mockReqConstructor.call_count, 1)
+        self.failUnlessEqual(mockReqConstructor.call_args[1]['parameters'], {'multi': ['1', '2']})
+
+        self.failUnless('multi=1' in mockHttpRequest.call_args[1]['body'])
+        self.failUnless('multi=2' in mockHttpRequest.call_args[1]['body'])
+
+if __name__ == "__main__":
+    unittest.main()

+ 29 - 0
desktop/core/src/desktop/auth/backend.py

@@ -177,6 +177,35 @@ class AllowFirstUserDjangoBackend(django.contrib.auth.backends.ModelBackend):
     return User.objects.count() == 0
 
 
+class OAuthBackend(DesktopBackendBase):
+  """
+  Heavily based on Twitter Oauth: https://github.com/simplegeo/python-oauth2#logging-into-django-w-twitter
+  Requires: python-oauth2 and httplib2
+
+  build/env/bin/python setup.py install https://github.com/simplegeo/python-oauth2
+  build/env/bin/pip install httplib2
+  """
+
+  def authenticate(self, access_token):
+    username = access_token['screen_name']
+    password = access_token['oauth_token_secret']
+
+    # Could save oauth_token detail in the user profile here
+
+    user = find_or_create_user(username, password)
+    user.is_superuser = False
+    user.save()
+
+    default_group = get_default_user_group()
+    if default_group is not None:
+      user.groups.add(default_group)
+
+    return user
+
+  @classmethod
+  def manages_passwords_externally(cls):
+    return True
+
 
 class AllowAllBackend(DesktopBackendBase):
   """

+ 56 - 7
desktop/core/src/desktop/auth/views.py

@@ -15,12 +15,20 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+
+try:
+  import oauth2 as oauth
+except:
+  pass
+
+import cgi
 import logging
+import urllib
 
 import django.contrib.auth.views
 from django.core import urlresolvers
 from django.core.exceptions import SuspiciousOperation
-from django.contrib.auth import login, get_backends
+from django.contrib.auth import login, get_backends, authenticate
 from django.contrib.auth.models import User
 from django.contrib.sessions.models import Session
 from django.http import HttpResponseRedirect
@@ -28,11 +36,12 @@ from django.utils.translation import ugettext as _
 from hadoop.fs.exceptions import WebHdfsException
 from useradmin.views import ensure_home_directory
 
-from desktop.auth.backend import AllowFirstUserDjangoBackend, AllowAllBackend
+from desktop.auth.backend import AllowFirstUserDjangoBackend
 from desktop.auth.forms import UserCreationForm, AuthenticationForm
 from desktop.lib.django_util import render
 from desktop.lib.django_util import login_notrequired
 from desktop.log.access import access_warn, last_access_map
+from desktop.conf import OAUTH
 
 LOG = logging.getLogger(__name__)
 
@@ -66,8 +75,8 @@ def first_login_ever():
   return False
 
 
-def is_allow_all_backend():
-  return get_backends() and isinstance(get_backends()[0], AllowAllBackend)
+def get_backend_name():
+  return get_backends() and get_backends()[0].__class__.__name__
 
 
 @login_notrequired
@@ -75,7 +84,7 @@ def dt_login(request):
   """Used by the non-jframe login"""
   redirect_to = request.REQUEST.get('next', '/')
   is_first_login_ever = first_login_ever()
-  is_allow_all = is_allow_all_backend()
+  backend_name = get_backend_name()
 
   if request.method == 'POST':
     # For first login, need to validate user info!
@@ -94,7 +103,7 @@ def dt_login(request):
         if request.session.test_cookie_worked():
           request.session.delete_test_cookie()
 
-        if is_first_login_ever or is_allow_all:
+        if is_first_login_ever or backend_name == 'AllowAllBackend':
           # Create home directory for first user.
           try:
             ensure_home_directory(request.fs, user.username)
@@ -119,7 +128,7 @@ def dt_login(request):
     'next': redirect_to,
     'first_login_ever': is_first_login_ever,
     'login_errors': request.method == 'POST',
-    'is_allow_all': is_allow_all
+    'backend_name': backend_name
   })
 
 
@@ -141,3 +150,43 @@ def _profile_dict(user):
     last_name=user.last_name,
     last_login=str(user.last_login), # datetime object needs to be converted
     email=user.email)
+
+
+# Oauth is based on Twitter as example.
+
+@login_notrequired
+def oauth_login(request):
+  consumer = oauth.Consumer(OAUTH.CONSUMER_KEY.get(), OAUTH.CONSUMER_SECRET.get())
+  client = oauth.Client(consumer)
+  resp, content = client.request(OAUTH.REQUEST_TOKEN_URL.get(), "POST", body=urllib.urlencode({
+                      'oauth_callback': 'http://' + request.get_host() + '/login/oauth_authenticated/'
+                  }))
+
+  if resp['status'] != '200':
+    raise Exception(_("Invalid response from Oauth provider: %s") % resp)
+
+  request.session['request_token'] = dict(cgi.parse_qsl(content))
+
+  url = "%s?oauth_token=%s" % (OAUTH.AUTHENTICATE_URL.get(), request.session['request_token']['oauth_token'])
+
+  return HttpResponseRedirect(url)
+
+
+@login_notrequired
+def oauth_authenticated(request):
+  consumer = oauth.Consumer(OAUTH.CONSUMER_KEY.get(), OAUTH.CONSUMER_SECRET.get())
+  token = oauth.Token(request.session['request_token']['oauth_token'], request.session['request_token']['oauth_token_secret'])
+  client = oauth.Client(consumer, token)
+
+  resp, content = client.request(OAUTH.ACCESS_TOKEN_URL.get(), "GET")
+  if resp['status'] != '200':
+      raise Exception(_("Invalid response from Oauth provider: %s") % resp)
+
+  access_token = dict(cgi.parse_qsl(content))
+
+  user = authenticate(access_token=access_token)
+  login(request, user)
+
+  redirect_to = request.REQUEST.get('next', '/')
+  return HttpResponseRedirect(redirect_to)
+

+ 43 - 2
desktop/core/src/desktop/conf.py

@@ -264,7 +264,7 @@ AUTH = ConfigSection(
     BACKEND=Config("backend",
                    default="desktop.auth.backend.AllowFirstUserDjangoBackend",
                    help=_("Authentication backend.  Common settings are "
-                        "django.contrib.auth.backends.ModelBackend (fully Django backend), " + 
+                        "django.contrib.auth.backends.ModelBackend (fully Django backend), " +
                         "desktop.auth.backend.AllowAllBackend (allows everyone), " +
                         "desktop.auth.backend.AllowFirstUserDjangoBackend (relies on Django and user manager, after the first login). ")),
     USER_AUGMENTOR=Config("user_augmentor",
@@ -349,6 +349,47 @@ LDAP = ConfigSection(
 ))
 
 
+OAUTH = ConfigSection(
+  key='oauth',
+  help=_('Configuration options for Oauth 1.0 authentication'),
+  members=dict(
+    CONSUMER_KEY = Config(
+      key="consumer_key",
+      help=_("The Consumer key of the application."),
+      type=str,
+      default="XXXXXXXXXXXXXXXXXXXXX"
+    ),
+
+    CONSUMER_SECRET = Config(
+      key="consumer_secret",
+      help=_("The Consumer secret of the application."),
+      type=str,
+      default="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
+    ),
+
+    REQUEST_TOKEN_URL = Config(
+      key="request_token_url",
+      help=_("The Request token URL."),
+      type=str,
+      default="https://api.twitter.com/oauth/request_token"
+    ),
+
+    ACCESS_TOKEN_URL = Config(
+      key="access_token_url",
+      help=_("The Access token URL."),
+      type=str,
+      default="https://api.twitter.com/oauth/access_token"
+    ),
+
+    AUTHENTICATE_URL = Config(
+      key="authenticate_url",
+      help=_("The Authorize URL."),
+      type=str,
+      default="https://api.twitter.com/oauth/authorize"
+    ),
+  )
+)
+
 
 LOCAL_FILESYSTEMS = UnspecifiedConfigSection(
   key="local_filesystems",
@@ -362,7 +403,7 @@ LOCAL_FILESYSTEMS = UnspecifiedConfigSection(
 def default_feedback_url():
   """A version-specific URL."""
   return "http://groups.google.com/a/cloudera.org/group/hue-user"
-  
+
 FEEDBACK_URL = Config(
   key="feedback_url",
   help=_("Link for 'feedback' tab."),

+ 1 - 0
desktop/core/src/desktop/lib/rest/http_client.py

@@ -164,6 +164,7 @@ class HttpClient(object):
     request.get_method = lambda: http_method
 
     headers = self._get_headers(headers)
+
     for k, v in headers.items():
       request.add_header(k, v)
 

+ 21 - 4
desktop/core/src/desktop/templates/login.mako

@@ -63,12 +63,16 @@ from django.utils.translation import ugettext as _
         <div class="row">
             <div class="span4 offset4">
                 <form method="POST" action="${action}" class="well">
-                    <label>${_('Username')}
+                    <label
+                    % if backend_name == 'OAuthBackend':
+                      class="hide"
+                    % endif
+                    >${_('Username')}
                         ${ form['username'] | n,unicode }
                         ${ form['username'].errors | n,unicode }
                     </label>
                     <label
-                    % if is_allow_all:
+                    % if backend_name in ('AllowAllBackend', 'OAuthBackend'):
                       class="hide"
                     % endif
                     >${_('Password')}
@@ -79,7 +83,7 @@ from django.utils.translation import ugettext as _
                     %if first_login_ever:
                         <input type="submit" class="btn btn-primary" value="${_('Sign up')}" />
                     %else:
-                        <input type="submit" class="btn btn-primary" value="${_('Sign in')}" />
+                        <input type="submit" class="btn btn-primary" value="${_('Sign in')}"/>
                     %endif
                     <input type="hidden" name="next" value="${next}" />
 
@@ -106,7 +110,7 @@ from django.utils.translation import ugettext as _
         %endif
     </div>
 
-% if is_allow_all:
+% if backend_name == 'AllowAllBackend':
   <script src="/static/ext/js/jquery/jquery-1.8.1.min.js"></script>
   <script>
     $(document).ready(function(){
@@ -115,5 +119,18 @@ from django.utils.translation import ugettext as _
   </script>
 % endif
 
+% if backend_name == 'OAuthBackend':
+  <script src="/static/ext/js/jquery/jquery-1.8.1.min.js"></script>
+  <script>
+  $(document).ready(function(){
+    $("input").css({"display": "block", "margin-left": "auto", "margin-right": "auto"});
+    $("input").bind('click', function() {
+      window.location.replace('/login/oauth/');
+      return false;
+    });
+  });
+  </script>
+% endif
+
 </body>
 </html>

+ 4 - 0
desktop/core/src/desktop/urls.py

@@ -61,6 +61,10 @@ dynamic_patterns = patterns('',
   (r'^debug/check_config_ajax$', 'desktop.views.check_config_ajax'),
   (r'^log_frontend_event$', 'desktop.views.log_frontend_event'),
 
+  # Oauth
+  (r'^login/oauth/?$', 'desktop.auth.views.oauth_login'),
+  (r'^login/oauth_authenticated/?$', 'desktop.auth.views.oauth_authenticated'),
+
   # Jasmine
   (r'^jasmine', 'desktop.views.jasmine'),