diff --git a/README.rst b/README.rst
index 58c52cd..59c6641 100644
--- a/README.rst
+++ b/README.rst
@@ -1,9 +1,7 @@
httpie-oauth
-===========
+============
-OAuth plugin for `HTTPie `_.
-
-It currently provides support for OAuth 1.0a 2-legged.
+OAuth 1.0a two-legged plugin for `HTTPie `_.
Installation
@@ -14,16 +12,83 @@ Installation
$ pip install httpie-oauth
-You should now see ``oauth1`` under ``--auth-type`` in ``$ http --help`` output.
+You should now see ``oauth1`` under ``--auth-type`` in the
+``$ http --help`` output.
+
+To be able to use the RSA-SHA1 signature type, also install **PyJWT**
+and PyCA's **cryptography** package.
+
+.. code-block:: bash
+
+ $ pip install pyjwt
+ $ pip install cryptography
+On CentOS 7, it might be easier to use *yum* to install "epel-release"
+and then the "python2-cryptography" packages, since to *pip install* it
+requires C code to be compiled.
Usage
-----
+HMAC-SHA1
+.........
+
+To use the HMAC-SHA1 signature method, in the ``--auth`` parameter
+provide the client-key, a single colon and the client-secret.
+
.. code-block:: bash
$ http --auth-type=oauth1 --auth='client-key:client-secret' example.org
+It will interactively prompt for the client-secret, if there is no colon.
+If the password starts with a colon, use this interactive method to enter it
+(otherwise the extra colon will cause it to use RSA-SHA1 instead of HMAC-SHA1).
+
+RSA-SHA1
+........
+
+To use the RSA-SHA1 signature method, in the ``--auth`` parameter
+provide the client key, two colons and the name of a file containing
+the RSA private key. The file must contain a PEM formatted RSA private
+key.
+
+.. code-block:: bash
+
+ $ http --auth-type=oauth1 --auth='client-key::filename' example.org
+
+It will interactively prompt for the filename, if there is no value
+after the two colons.
+
+The filename can also be a relative or absolute path to the file.
+
+Passphrase protected private keys are not supported.
+
+Including the client key in the private key file
+++++++++++++++++++++++++++++++++++++++++++++++++
+
+If the client key in the ``--auth`` parameter is empty (i.e. the
+option argument is just two colons and the filename), the
+``oauth_consumer_key`` parameter from the file is used. It must
+appear in the file before the private key.
+
+For example, if the private key file contains something like this:
+
+::
+
+ oauth_consumer_key: myconsumerkey
+ -----BEGIN RSA PRIVATE KEY-----
+ ...
+ -----END RSA PRIVATE KEY-----
+
+It can be used with this command:
+
+.. code-block:: bash
+
+ $ http --auth-type=oauth1 --auth=::filename example.org
+
+
+HTTPie Sessions
+...............
You can also use `HTTPie sessions `_:
@@ -35,3 +100,16 @@ You can also use `HTTPie sessions `_:
# Re-use auth
$ http --session=logged-in POST example.org hello=world
+
+Troubleshooting
+...............
+
+ImportError: No module named jwt.algorithms
++++++++++++++++++++++++++++++++++++++++++++
+
+The *PyJWT* module is not available. Please install it.
+
+AttributeError: 'module' object has no attribute 'RSAAlgorithm'
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+The PyCA's *cryptography* module is not available. Please install it.
diff --git a/httpie_oauth.py b/httpie_oauth.py
index 47cdc45..acec84d 100644
--- a/httpie_oauth.py
+++ b/httpie_oauth.py
@@ -1,12 +1,26 @@
"""
-OAuth plugin for HTTPie.
+OAuth 1.0a 2-legged plugin for HTTPie.
+
+Supports HMAC-SHA1 and RSA-SHA1 signature methods.
+
+If the authentication parameter is "username:password" then HMAC-SHA1 is used.
+If the password is omitted (i.e. --auth username is provided), the user is
+prompted for the password.
+
+If the authentication parameter is "username::filename" (double colon between
+the username and the filename) then RSA-SHA1 is used, and the PEM formatted
+private key is read from that file. If the filename is omitted
+(i.e. --auth username:: is provided), the user is prompted for
+the filename. The username is used as the oauth_client_key OAuth parameter.
"""
+import string
+import sys
from httpie.plugins import AuthPlugin
from requests_oauthlib import OAuth1
+from oauthlib.oauth1 import SIGNATURE_RSA
-
-__version__ = '1.0.2'
+__version__ = '2.0.0'
__author__ = 'Jakub Roztocil'
__licence__ = 'BSD'
@@ -15,7 +29,147 @@ class OAuth1Plugin(AuthPlugin):
name = 'OAuth 1.0a 2-legged'
auth_type = 'oauth1'
- description = ''
+ description =\
+ '--auth user:HMAC-SHA1_secret or --auth user::RSA-SHA1_privateKeyFile'
+
+ def get_auth(self, username=None, password=None):
+ """
+ Generate OAuth 1.0a 2-legged authentication for HTTPie.
+
+ Note: Passpharse protected private keys are not yet supported.
+ Before support can be implemented, the PyJWT, oauthlib and
+ requests_oauthlib modules need to be updated.
+ The passphrase needs to be obtained here and passed
+ through to PyJWT's jwt/algorithms.py, line 168, where currently it
+ passes into load_pem_private_key a hardcoded value of None for the
+ password.
+ To get it to there, many places in oauthlib's oauth1/rfc5849/__init__.py
+ and oauth1/rfc5849/signature.py, as well as in requests_oauthlib's
+ oauth1_auth.py, need to be updated to pass it through.
+
+ :param username: username
+ :param password: password, or colon followed by a filename
+ :return: requests_oauthlib.oauth1_auth.OAuth1 object
+ """
+ if not password.startswith(':'):
+ # HMAC-SHA1 signature method (--auth client-key:client-secret)
+ return OAuth1(client_key=username, client_secret=password)
+
+ else:
+ # RSA-SHA1 signature method (--auth oauth_consumer_key::filename)
+ filename = password[1:]
+ if len(filename) == 0:
+ # Prompt for filename of RSA private key
+ try:
+ filename = raw_input('http: filename of RSA private key: ')
+ except EOFError: # if ^D entered
+ sys.exit(1)
+
+ username, key = OAuth1Plugin.read_private_key(username, filename)
+
+ return OAuth1(client_key=username,
+ signature_method=SIGNATURE_RSA,
+ rsa_key=key)
+
+ @staticmethod
+ def read_private_key(username, filename):
+ """
+ Check if the key is a recognised private key format.
+
+ Prints an error message to stderr and exits if it is not. Uses
+ crude checks to try and generate more useful error messages.
+
+ :param username: username to use
+ :param filename: file to read private key from
+ :return: PEM formatted private key
+ """
+ PEM_PRIVATE_KEY_BEGINNING = '-----BEGIN RSA PRIVATE KEY-----'
+ PEM_PRIVATE_KEY_ENDING = '-----END RSA PRIVATE KEY-----'
+ ATTR_NAME = 'oauth_consumer_key'
+
+ try:
+ data = open(filename).read()
+
+ key_start = data.find(PEM_PRIVATE_KEY_BEGINNING)
+ key_end = data.find(PEM_PRIVATE_KEY_ENDING)
+
+ if key_start == -1:
+ # Did not find the start of private key.
+
+ if data.find('-----BEGIN PUBLIC KEY-----') != -1 or \
+ data.find('-----BEGIN RSA PUBLIC KEY-----') != -1 or \
+ data.find('ssh-rsa ') != -1 or \
+ data.find('---- BEGIN SSH2 PUBLIC KEY ----') != -1:
+ # Appears to contain a PKCS8, PEM, OpenSSH old or
+ # OpenSSH new format public key.
+ # The newer OpenSSH format does not follow RFC7468
+ # and only has 4 hyphens and spaces around the text!
+ err = 'wrong key, please provide the PRIVATE key'
+ elif data.find('-----BEGIN OPENSSH PRIVATE KEY-----') != -1:
+ # Appears to contain newer OpenSSH private key format
+ err = 'private key format not supported' + \
+ ', PEM format required'
+ else:
+ # Generic error message
+ err = 'does not contain a PEM formatted private key'
+
+ else:
+ if key_end == -1:
+ err = 'private key is incomplete'
+ else:
+ key_end += len(PEM_PRIVATE_KEY_ENDING)
+ err = None
+
+ # If the username is blank, try to extract a username from the file
+
+ if len(username) == 0 and err is None:
+ username, err = OAuth1Plugin.extract_username(data, ATTR_NAME,
+ 0, key_start)
+
+ if err is not None:
+ sys.stderr.write("http: " + filename + ': ' + err)
+ sys.exit(1)
+
+ pem_key = data[key_start:key_end]
+
+ return username, pem_key
+
+ except IOError as e:
+ sys.stderr.write("http: " + str(e))
+ sys.exit(1)
+
+ @staticmethod
+ def extract_username(data, attr_name, start, end, limit=8096):
+ """
+ Extract a named parameter from the contents.
+
+ :param data: text to search
+ :param attr_name: name of attribute to look for
+ :param start: index into contents of where to start looking
+ :param end: index into contents of where to stop looking
+ :param limit: upper limit for end position
+ :return: client-key value or None if not found
+ """
+ stop_pos = end
+ if limit < end:
+ stop_pos = limit
+ i = start
+
+ while i < stop_pos:
+ eol_pos = data.find('\n', i, stop_pos)
+ if eol_pos == -1:
+ break # no more complete lines found
+
+ colon_pos = data.find(':', i, eol_pos)
+ if colon_pos != -1:
+ name = data[i:colon_pos].strip()
+ value = data[colon_pos + 1: eol_pos].strip()
+ if name == attr_name:
+ return value, None # successfully found
+ i = eol_pos + 1
- def get_auth(self, username, password):
- return OAuth1(client_key=username, client_secret=password)
+ if limit < end:
+ return None, '"{}" not found in first {} characters'.format(
+ attr_name, limit)
+ else:
+ return None, '"{}" not found before private key'.format(attr_name)
diff --git a/setup.py b/setup.py
index 33f58f8..2d779d2 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
name='httpie-oauth',
description='OAuth plugin for HTTPie.',
long_description=open('README.rst').read().strip(),
- version='1.0.2',
+ version='2.0.0',
author='Jakub Roztocil',
author_email='jakub@roztocil.name',
license='BSD',