From 2acb30355200369137643fdc4d7e1bb11572fa42 Mon Sep 17 00:00:00 2001 From: Jakub Roztocil Date: Sat, 21 Sep 2013 23:46:15 +0200 Subject: [PATCH] Added support for auth plugins. --- README.rst | 8 ++++++++ httpie/__init__.py | 2 +- httpie/cli.py | 26 +++++++++++++++++++++++--- httpie/client.py | 14 ++++++++++---- httpie/core.py | 5 ++++- httpie/input.py | 2 +- httpie/plugins/__init__.py | 9 +++++++++ httpie/plugins/base.py | 28 ++++++++++++++++++++++++++++ httpie/plugins/builtin.py | 26 ++++++++++++++++++++++++++ httpie/plugins/manager.py | 35 +++++++++++++++++++++++++++++++++++ httpie/sessions.py | 29 ++++++++++++++--------------- 11 files changed, 159 insertions(+), 25 deletions(-) create mode 100644 httpie/plugins/__init__.py create mode 100644 httpie/plugins/base.py create mode 100644 httpie/plugins/builtin.py create mode 100644 httpie/plugins/manager.py diff --git a/README.rst b/README.rst index de45f00d..1d368cc7 100644 --- a/README.rst +++ b/README.rst @@ -514,6 +514,13 @@ Authorization information from your ``.netrc`` file is honored as well: [...] +------- +Plugins +------- + +* `httpie-ntlm `_ + + ======= Proxies ======= @@ -1206,6 +1213,7 @@ Changelog * `0.7.0-dev`_ * Added ``--ignore-stdin``. + * Added support for auth plugins. * `0.6.0`_ (2013-06-03) * XML data is now formatted. * ``--session`` and ``--session-read-only`` now also accept paths to diff --git a/httpie/__init__.py b/httpie/__init__.py index 568f48eb..592bd0ca 100644 --- a/httpie/__init__.py +++ b/httpie/__init__.py @@ -3,7 +3,7 @@ HTTPie - a CLI, cURL-like tool for humans. """ __author__ = 'Jakub Roztocil' -__version__ = '0.6.0' +__version__ = '0.7.0' __licence__ = 'BSD' diff --git a/httpie/cli.py b/httpie/cli.py index 88859dfd..16422533 100644 --- a/httpie/cli.py +++ b/httpie/cli.py @@ -10,6 +10,8 @@ from argparse import (RawDescriptionHelpFormatter, FileType, from . import __doc__ from . import __version__ +from .plugins.builtin import BuiltinAuthPlugin +from .plugins import plugin_manager from .sessions import DEFAULT_SESSIONS_DIR from .output import AVAILABLE_STYLES, DEFAULT_STYLE from .input import (Parser, AuthCredentialsArgType, KeyValueArgType, @@ -381,14 +383,32 @@ auth.add_argument( """, ) +_auth_plugins = plugin_manager.get_auth_plugins() auth.add_argument( '--auth-type', - choices=['basic', 'digest'], - default='basic', + choices=[plugin.auth_type for plugin in _auth_plugins], + default=_auth_plugins[0].auth_type, help=""" - The authentication mechanism to be used. Defaults to "basic". + The authentication mechanism to be used. Defaults to "{default}". + + {types} """ + .format(default=_auth_plugins[0].auth_type, types='\n '.join( + '"{type}": {name}{package}{description}'.format( + type=plugin.auth_type, + name=plugin.name, + package=( + '' if issubclass(plugin, BuiltinAuthPlugin) + else ' (provided by %s)' % plugin.package_name + ), + description=( + '' if not plugin.description else + '\n ' + ('\n '.join(wrap(plugin.description))) + ) + ) + for plugin in _auth_plugins + )), ) diff --git a/httpie/client.py b/httpie/client.py index 683b9535..5c0488f8 100644 --- a/httpie/client.py +++ b/httpie/client.py @@ -7,6 +7,7 @@ import requests.auth from . import sessions from . import __version__ +from .plugins import plugin_manager FORM = 'application/x-www-form-urlencoded; charset=utf-8' @@ -14,6 +15,12 @@ JSON = 'application/json; charset=utf-8' DEFAULT_UA = 'HTTPie/%s' % __version__ +class HTTPie(object): + + def __init__(self, env, plugin_manager): + pass + + def get_response(args, config_dir): """Send the request and return a `request.Response`.""" @@ -27,6 +34,7 @@ def get_response(args, config_dir): response = requests.request(**requests_kwargs) else: response = sessions.get_response( + args=args, config_dir=config_dir, session_name=args.session or args.session_read_only, requests_kwargs=requests_kwargs, @@ -69,10 +77,8 @@ def get_requests_kwargs(args): credentials = None if args.auth: - credentials = { - 'basic': requests.auth.HTTPBasicAuth, - 'digest': requests.auth.HTTPDigestAuth, - }[args.auth_type](args.auth.key, args.auth.value) + auth_plugin = plugin_manager.get_auth_plugin(args.auth_type)() + credentials = auth_plugin.get_auth(args.auth.key, args.auth.value) kwargs = { 'stream': True, diff --git a/httpie/core.py b/httpie/core.py index f442ee0c..fbcd7a3a 100644 --- a/httpie/core.py +++ b/httpie/core.py @@ -18,13 +18,13 @@ from httpie import __version__ as httpie_version from requests import __version__ as requests_version from pygments import __version__ as pygments_version -from .cli import parser from .compat import str, is_py3 from .client import get_response from .downloads import Download from .models import Environment from .output import build_output_stream, write, write_with_colors_win_py3 from . import ExitStatus +from .plugins import plugin_manager def get_exit_status(http_status, follow=False): @@ -58,6 +58,9 @@ def main(args=sys.argv[1:], env=Environment()): Return exit status code. """ + plugin_manager.load_installed_plugins() + from .cli import parser + if env.config.default_options: args = env.config.default_options + args diff --git a/httpie/input.py b/httpie/input.py index 22c79b41..5a512aa8 100644 --- a/httpie/input.py +++ b/httpie/input.py @@ -6,7 +6,7 @@ import sys import re import json import mimetypes -import getpass +from getpass import getpass from io import BytesIO #noinspection PyCompatibility from argparse import ArgumentParser, ArgumentTypeError, ArgumentError diff --git a/httpie/plugins/__init__.py b/httpie/plugins/__init__.py new file mode 100644 index 00000000..f7b1dffa --- /dev/null +++ b/httpie/plugins/__init__.py @@ -0,0 +1,9 @@ +from .base import AuthPlugin +from .manager import PluginManager +from .builtin import BasicAuthPlugin, DigestAuthPlugin + + +plugin_manager = PluginManager() +plugin_manager.register(BasicAuthPlugin) +plugin_manager.register(DigestAuthPlugin) + diff --git a/httpie/plugins/base.py b/httpie/plugins/base.py new file mode 100644 index 00000000..3afbc71e --- /dev/null +++ b/httpie/plugins/base.py @@ -0,0 +1,28 @@ +class AuthPlugin(object): + """ + Base auth plugin class. + + See for an example auth plugin. + + """ + + # The value that should be passed to --auth-type + # to use this auth plugin. Eg. "my-auth" + auth_type = None + + # The name of the plugin, eg. "My auth". + name = None + + # Optional short description. Will be be shown in the help + # under --auth-type. + description = None + + # This be set automatically once the plugin has been loaded. + package_name = None + + def get_auth(self, username, password): + """ + Return a ``requests.auth.AuthBase`` subclass instance. + + """ + raise NotImplementedError() diff --git a/httpie/plugins/builtin.py b/httpie/plugins/builtin.py new file mode 100644 index 00000000..530c1405 --- /dev/null +++ b/httpie/plugins/builtin.py @@ -0,0 +1,26 @@ +import requests.auth + +from .base import AuthPlugin + + +class BuiltinAuthPlugin(AuthPlugin): + + package_name = '(builtin)' + + +class BasicAuthPlugin(BuiltinAuthPlugin): + + name = 'Basic HTTP auth' + auth_type = 'basic' + + def get_auth(self, username, password): + return requests.auth.HTTPBasicAuth(username, password) + + +class DigestAuthPlugin(BuiltinAuthPlugin): + + name = 'Digest HTTP auth' + auth_type = 'digest' + + def get_auth(self, username, password): + return requests.auth.HTTPDigestAuth(username, password) diff --git a/httpie/plugins/manager.py b/httpie/plugins/manager.py new file mode 100644 index 00000000..61b1a384 --- /dev/null +++ b/httpie/plugins/manager.py @@ -0,0 +1,35 @@ +from pkg_resources import iter_entry_points + + +ENTRY_POINT_NAMES = [ + 'httpie.plugins.auth.v1' +] + + +class PluginManager(object): + + def __init__(self): + self._plugins = [] + + def __iter__(self): + return iter(self._plugins) + + def register(self, plugin): + self._plugins.append(plugin) + + def get_auth_plugins(self): + return list(self._plugins) + + def get_auth_plugin_mapping(self): + return dict((plugin.auth_type, plugin) for plugin in self) + + def get_auth_plugin(self, auth_type): + return self.get_auth_plugin_mapping()[auth_type] + + def load_installed_plugins(self): + + for entry_point_name in ENTRY_POINT_NAMES: + for entry_point in iter_entry_points(entry_point_name): + plugin = entry_point.load() + plugin.package_name = entry_point.dist.key + self.register(entry_point.load()) diff --git a/httpie/sessions.py b/httpie/sessions.py index ead383a5..b9fe21e9 100644 --- a/httpie/sessions.py +++ b/httpie/sessions.py @@ -6,10 +6,10 @@ import os import requests from requests.cookies import RequestsCookieJar, create_cookie -from requests.auth import HTTPBasicAuth, HTTPDigestAuth from .compat import urlsplit from .config import BaseConfigDict, DEFAULT_CONFIG_DIR +from httpie.plugins import plugin_manager SESSIONS_DIR_NAME = 'sessions' @@ -21,7 +21,8 @@ VALID_SESSION_NAME_PATTERN = re.compile('^[a-zA-Z0-9_.-]+$') SESSION_IGNORED_HEADER_PREFIXES = ['Content-', 'If-'] -def get_response(session_name, requests_kwargs, config_dir, read_only=False): +def get_response(session_name, requests_kwargs, config_dir, args, + read_only=False): """Like `client.get_response`, but applies permanent aspects of the session to the request. @@ -50,9 +51,12 @@ def get_response(session_name, requests_kwargs, config_dir, read_only=False): requests_kwargs['headers'] = dict(session.headers, **request_headers) session.update_headers(request_headers) - auth = requests_kwargs.get('auth', None) - if auth: - session.auth = auth + if args.auth: + session.auth = { + 'type': args.auth_type, + 'username': args.auth.key, + 'password': args.auth.value, + } elif session.auth: requests_kwargs['auth'] = session.auth @@ -140,15 +144,10 @@ class Session(BaseConfigDict): auth = self.get('auth', None) if not auth or not auth['type']: return - Auth = {'basic': HTTPBasicAuth, - 'digest': HTTPDigestAuth}[auth['type']] - return Auth(auth['username'], auth['password']) + auth_plugin = plugin_manager.get_auth_plugin(auth['type'])() + return auth_plugin.get_auth(auth['username'], auth['password']) @auth.setter - def auth(self, cred): - self['auth'] = { - 'type': {HTTPBasicAuth: 'basic', - HTTPDigestAuth: 'digest'}[type(cred)], - 'username': cred.username, - 'password': cred.password, - } + def auth(self, auth): + assert set(['type', 'username', 'password']) == set(auth.keys()) + self['auth'] = auth