Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth 1.0a a.k.a. OAuth 1.0 a.k.a. RFC 5849 #10

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 170 additions & 11 deletions curlish.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from getpass import getpass
from uuid import UUID
import logging

try:
from oauthlib.oauth1.rfc5849 import Client as OAuth1
except ImportError:
class OAuth1(object):
def __init__(*args, **kwargs):
raise NotImplementedError('RFC 5849 authorization requires oauthlib')


def str_to_uuid(s):
Expand Down Expand Up @@ -221,6 +229,12 @@ def find_url_arg(arguments):
if arg.startswith(('http:', 'https:')):
return idx

def find_method_arg(arguments):
"""Finds the HTTP method argument in acurl argument list."""
for idx, arg in enumerate(arguments):
if arg == '-X':
return idx + 1


class AuthorizationHandler(BaseHTTPRequestHandler):
"""Callback handler for the code based authorization"""
Expand All @@ -231,7 +245,8 @@ def do_GET(self):
self.end_headers()
self.server.token_response = dict((k, v[-1]) for k, v in
cgi.parse_qs(self.path.split('?')[-1]).iteritems())
if 'code' in self.server.token_response:
if 'code' in self.server.token_response or \
'oauth_verifier' in self.server.token_response:
title = 'Tokens Received'
text = 'The tokens were transmitted successfully to curlish.'
else:
Expand Down Expand Up @@ -301,7 +316,10 @@ def _full_url(url):
return url
self.name = name
self.base_url = values.get('base_url')
self.oauth_version = values.get('oauth_version', '2.0')
self.grant_type = values.get('grant_type', 'authorization_code')
self.signature_type = values.get('signature_type', 'AUTH_HEADER')
self.request_token_url = _full_url(values.get('request_token_url'))
self.access_token_url = _full_url(values.get('access_token_url'))
self.authorize_url = _full_url(values.get('authorize_url'))
self.client_id = values.get('client_id')
Expand All @@ -311,6 +329,7 @@ def _full_url(url):
self.bearer_transmission = values.get('bearer_transmission', 'query')
self.default = values.get('default', False)
self.access_token = None
self.access_token_secret = None

def make_request(self, method, url, headers=None, data=None):
"""Makes an HTTP request to the site."""
Expand All @@ -330,17 +349,26 @@ def make_request(self, method, url, headers=None, data=None):
real_headers = self.extra_headers.copy()
real_headers.update(headers or ())

conn.request(method, u.path, data, real_headers)
uri = u.path + ('?' + u.query if u.query else '')
logger.debug('Request: {0} {1}'.format(method, url))
logger.debug('Request headers: {0}'.format(real_headers))
logger.debug('Request body: {0}'.format(data))
conn.request(method, uri, data, real_headers)
resp = conn.getresponse()
resp_body = resp.read()
logger.debug('Response status: {0}'.format(resp.status))
logger.debug('Response headers: {0}'.format(resp.getheaders()))
logger.debug('Response body: {0}'.format(resp_body))

ct = resp.getheader('Content-Type')
if ct.startswith('application/json') or ct.startswith('text/javascript'):
resp_data = json.loads(resp.read())
elif ct.startswith('text/html'):
fail('Invalid response from server: ' + resp.read())
resp_data = json.loads(resp_body)
else:
if not ct.startswith('application/x-www-form-urlencoded'):
logger.info('Unexpected Content-Type from server: ' + ct + '. Trying to continue anyway.')

resp_data = dict((k, v[-1]) for k, v in
cgi.parse_qs(resp.read()).iteritems())
cgi.parse_qs(resp_body).iteritems())

return resp.status, resp_data

Expand Down Expand Up @@ -413,6 +441,101 @@ def request_authorization_code_grant(self):
print ' %s: %s' % (key, value)
sys.exit(1)

def get_rfc5849_request_token(self, params, headers):
"""Tries to load tokens with the given parameters."""
data = params
status, data = self.make_request('POST',
self.request_token_url, data=data, headers=headers)

if status >= 200 and status < 300:
return data
error = data.get('error')
if error in ('invalid_grant', 'access_denied'):
return None
error_msg = data.get('error_description')
fail("Couldn't authorize: %s - %s" % (error, error_msg))

def request_rfc5849_authorization_code_grant(self):
redirect_uri = u'http://127.0.0.1:%d/' % settings.values['http_port']
oauth = OAuth1(self.client_id, self.client_secret, callback_uri=redirect_uri,
signature_type=self.signature_type)
(request_token_url, headers, body) = oauth.sign(
unicode(self.request_token_url),
u'POST',
body='',
headers={'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/x-www-form-urlencoded'},
realm=None)

rdata = self.get_rfc5849_request_token(body, headers)

logger.debug('Temporary credentials response: {0}'.format(rdata))

params = {
'oauth_token': rdata['oauth_token'],
}
params.update(self.request_token_params)
browser_url = '%s?%s' % (
self.authorize_url,
urllib.urlencode(params)
)

logger.debug("Confirm the user's authorization via: {0}".format(browser_url))

webbrowser.open(browser_url)
server_address = ('127.0.0.1', settings.values['http_port'])
httpd = HTTPServer(server_address, AuthorizationHandler)
httpd.token_response = None
httpd.handle_request()
logger.debug('{0}'.format(httpd.token_response))
if 'oauth_verifier' in httpd.token_response:
return self.exchange_rfc5849_verifier_for_access_token(
{ 'oauth_verifier': httpd.token_response['oauth_verifier'] },
rdata['oauth_token'],
rdata['oauth_token_secret'],
redirect_uri)

print 'Could not sign in: grant cancelled'
for key, value in httpd.token_response.iteritems():
print ' %s: %s' % (key, value)
sys.exit(1)

def exchange_rfc5849_verifier_for_access_token(self, params, request_token,
request_token_secret,
redirect_uri):
settings.values['rfc5849_token_cache'][self.name] = self.get_rfc5849_access_token(
params, request_token, request_token_secret, redirect_uri)

def get_rfc5849_access_token(self, params, request_token,
request_token_secret, redirect_uri):
"""Tries to load tokens with the given parameters."""
data = params.copy()

oauth = OAuth1(self.client_id, self.client_secret,
unicode(request_token),
unicode(request_token_secret),
callback_uri=redirect_uri, signature_type=self.signature_type)
(access_token_url, headers, body) = oauth.sign(
unicode(self.access_token_url),
u'POST',
body=data,
headers={'Content-Type': 'application/x-www-form-urlencoded'},
realm=None)

status, data = self.make_request('POST',
self.access_token_url, data=body, headers=headers)

if status >= 200 and status < 300:
return {
'access_token': unicode(data['oauth_token']),
'access_token_secret': unicode(data['oauth_token_secret']),
}
error = data.get('error')
if error in ('invalid_grant', 'access_denied'):
return None
error_msg = data.get('error_description')
fail("Couldn't authorize: %s - %s" % (error, error_msg))

def exchange_code_for_token(self, code, redirect_uri):
settings.values['token_cache'][self.name] = self.get_access_token({
'code': code,
Expand All @@ -427,15 +550,25 @@ def request_tokens(self):
self.request_authorization_code_grant()
elif self.grant_type == 'client_credentials':
self.request_client_credentials_grant()
elif self.grant_type == 'rfc5849_authorization_code':
self.request_rfc5849_authorization_code_grant()
else:
fail('Invalid grant configured: %s' % self.grant_type)

def fetch_token_if_necessarys(self):
token_cache = settings.values['token_cache']
if token_cache.get(self.name) is None:
self.request_tokens()
self.access_token = token_cache[self.name]

if self.oauth_version == '2.0':
token_cache = settings.values['token_cache']
if token_cache.get(self.name) is None:
self.request_tokens()
self.access_token = token_cache[self.name]
elif self.oauth_version == 'rfc5849':
token_cache = settings.values['rfc5849_token_cache']
if token_cache.get(self.name) is None:
self.request_tokens()
self.access_token = token_cache[self.name]['access_token']
self.access_token_secret = token_cache[self.name]['access_token_secret']
else:
fail('Invalid OAuth version configured: %s' % self.oauth_version)

def get_site_by_name(name):
"""Finds a site by its name."""
Expand Down Expand Up @@ -925,6 +1058,9 @@ def main():
parser.add_argument('--clear-cookies', action='store_true',
help='Deletes all the cookies or cookies that belong '
'to one specific site only.')
parser.add_argument('--logging-level',
help='Log output with the given logging level, '
'e.g., DEBUG, INFO, etc.')
parser.add_argument('--dump-curl-args', action='store_true',
help='Instead of executing dump the curl command line '
'arguments for this call')
Expand Down Expand Up @@ -959,6 +1095,9 @@ def main():
clear_cookies(args.site)
return

if args.logging_level:
logging.basicConfig(level=args.logging_level)

# Redirect everything else to curl via the site
url_arg = find_url_arg(extra_args)
if url_arg is None:
Expand All @@ -968,10 +1107,30 @@ def main():
if site is not None and site.grant_type is not None:
site.fetch_token_if_necessarys()
settings.save()
if site.oauth_version == 'rfc5849':
oauth = OAuth1(
site.client_id,
site.client_secret,
site.access_token,
site.access_token_secret,
signature_type='QUERY')
method_arg = find_method_arg(extra_args)
if method_arg is None:
method = 'GET'
else:
method = extra_args[method_arg]
(extra_args[url_arg], headers, body) = oauth.sign(
unicode(extra_args[url_arg]),
unicode(method),
body=None,
headers=None,
realm=None)
logger.debug('Signed request URL: {0}'.format(extra_args[url_arg]))
invoke_curl(site, settings.values['curl_path'], extra_args, url_arg,
dump_args=args.dump_curl_args,
dump_response=args.dump_response)

logger = logging.getLogger(__name__)

if __name__ == '__main__':
try:
Expand Down