mirror of https://github.com/pallets/flask.git
add HTTPS support for flask run command
This commit is contained in:
parent
c3a997864e
commit
2beedabaaf
3
CHANGES
3
CHANGES
|
@ -118,6 +118,8 @@ Major release, unreleased
|
||||||
- The dev server now uses threads by default. (`#2529`_)
|
- The dev server now uses threads by default. (`#2529`_)
|
||||||
- Loading config files with ``silent=True`` will ignore ``ENOTDIR``
|
- Loading config files with ``silent=True`` will ignore ``ENOTDIR``
|
||||||
errors. (`#2581`_)
|
errors. (`#2581`_)
|
||||||
|
- Pass ``--cert`` and ``--key`` options to ``flask run`` to run the
|
||||||
|
development server over HTTPS. (`#2606`_)
|
||||||
|
|
||||||
.. _pallets/meta#24: https://github.com/pallets/meta/issues/24
|
.. _pallets/meta#24: https://github.com/pallets/meta/issues/24
|
||||||
.. _#1421: https://github.com/pallets/flask/issues/1421
|
.. _#1421: https://github.com/pallets/flask/issues/1421
|
||||||
|
@ -154,6 +156,7 @@ Major release, unreleased
|
||||||
.. _#2450: https://github.com/pallets/flask/pull/2450
|
.. _#2450: https://github.com/pallets/flask/pull/2450
|
||||||
.. _#2529: https://github.com/pallets/flask/pull/2529
|
.. _#2529: https://github.com/pallets/flask/pull/2529
|
||||||
.. _#2581: https://github.com/pallets/flask/pull/2581
|
.. _#2581: https://github.com/pallets/flask/pull/2581
|
||||||
|
.. _#2606: https://github.com/pallets/flask/pull/2606
|
||||||
|
|
||||||
|
|
||||||
Version 0.12.3
|
Version 0.12.3
|
||||||
|
|
93
flask/cli.py
93
flask/cli.py
|
@ -14,6 +14,7 @@ import ast
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
from functools import update_wrapper
|
from functools import update_wrapper
|
||||||
|
@ -21,9 +22,10 @@ from operator import attrgetter
|
||||||
from threading import Lock, Thread
|
from threading import Lock, Thread
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
from werkzeug.utils import import_string
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from ._compat import getargspec, iteritems, reraise
|
from ._compat import getargspec, iteritems, reraise, text_type
|
||||||
from .globals import current_app
|
from .globals import current_app
|
||||||
from .helpers import get_debug_flag, get_env
|
from .helpers import get_debug_flag, get_env
|
||||||
|
|
||||||
|
@ -599,11 +601,96 @@ def show_server_banner(env, debug, app_import_path):
|
||||||
print(' * Debug mode: {0}'.format('on' if debug else 'off'))
|
print(' * Debug mode: {0}'.format('on' if debug else 'off'))
|
||||||
|
|
||||||
|
|
||||||
|
class CertParamType(click.ParamType):
|
||||||
|
"""Click option type for the ``--cert`` option. Allows either an
|
||||||
|
existing file, the string ``'adhoc'``, or an import for a
|
||||||
|
:class:`~ssl.SSLContext` object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = 'path'
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.path_type = click.Path(
|
||||||
|
exists=True, dir_okay=False, resolve_path=True)
|
||||||
|
|
||||||
|
def convert(self, value, param, ctx):
|
||||||
|
try:
|
||||||
|
return self.path_type(value, param, ctx)
|
||||||
|
except click.BadParameter:
|
||||||
|
value = click.STRING(value, param, ctx).lower()
|
||||||
|
|
||||||
|
if value == 'adhoc':
|
||||||
|
try:
|
||||||
|
import OpenSSL
|
||||||
|
except ImportError:
|
||||||
|
raise click.BadParameter(
|
||||||
|
'Using ad-hoc certificates requires pyOpenSSL.',
|
||||||
|
ctx, param)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
obj = import_string(value, silent=True)
|
||||||
|
|
||||||
|
if sys.version_info < (2, 7):
|
||||||
|
if obj:
|
||||||
|
return obj
|
||||||
|
else:
|
||||||
|
if isinstance(obj, ssl.SSLContext):
|
||||||
|
return obj
|
||||||
|
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_key(ctx, param, value):
|
||||||
|
"""The ``--key`` option must be specified when ``--cert`` is a file.
|
||||||
|
Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed.
|
||||||
|
"""
|
||||||
|
cert = ctx.params.get('cert')
|
||||||
|
is_adhoc = cert == 'adhoc'
|
||||||
|
|
||||||
|
if sys.version_info < (2, 7):
|
||||||
|
is_context = cert and not isinstance(cert, (text_type, bytes))
|
||||||
|
else:
|
||||||
|
is_context = isinstance(cert, ssl.SSLContext)
|
||||||
|
|
||||||
|
if value is not None:
|
||||||
|
if is_adhoc:
|
||||||
|
raise click.BadParameter(
|
||||||
|
'When "--cert" is "adhoc", "--key" is not used.',
|
||||||
|
ctx, param)
|
||||||
|
|
||||||
|
if is_context:
|
||||||
|
raise click.BadParameter(
|
||||||
|
'When "--cert" is an SSLContext object, "--key is not used.',
|
||||||
|
ctx, param)
|
||||||
|
|
||||||
|
if not cert:
|
||||||
|
raise click.BadParameter(
|
||||||
|
'"--cert" must also be specified.',
|
||||||
|
ctx, param)
|
||||||
|
|
||||||
|
ctx.params['cert'] = cert, value
|
||||||
|
|
||||||
|
else:
|
||||||
|
if cert and not (is_adhoc or is_context):
|
||||||
|
raise click.BadParameter(
|
||||||
|
'Required when using "--cert".',
|
||||||
|
ctx, param)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
@click.command('run', short_help='Runs a development server.')
|
@click.command('run', short_help='Runs a development server.')
|
||||||
@click.option('--host', '-h', default='127.0.0.1',
|
@click.option('--host', '-h', default='127.0.0.1',
|
||||||
help='The interface to bind to.')
|
help='The interface to bind to.')
|
||||||
@click.option('--port', '-p', default=5000,
|
@click.option('--port', '-p', default=5000,
|
||||||
help='The port to bind to.')
|
help='The port to bind to.')
|
||||||
|
@click.option('--cert', type=CertParamType(),
|
||||||
|
help='Specify a certificate file to use HTTPS.')
|
||||||
|
@click.option('--key',
|
||||||
|
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
|
||||||
|
callback=_validate_key, expose_value=False,
|
||||||
|
help='The key file to use when specifying a certificate.')
|
||||||
@click.option('--reload/--no-reload', default=None,
|
@click.option('--reload/--no-reload', default=None,
|
||||||
help='Enable or disable the reloader. By default the reloader '
|
help='Enable or disable the reloader. By default the reloader '
|
||||||
'is active if debug is enabled.')
|
'is active if debug is enabled.')
|
||||||
|
@ -617,7 +704,7 @@ def show_server_banner(env, debug, app_import_path):
|
||||||
help='Enable or disable multithreading.')
|
help='Enable or disable multithreading.')
|
||||||
@pass_script_info
|
@pass_script_info
|
||||||
def run_command(info, host, port, reload, debugger, eager_loading,
|
def run_command(info, host, port, reload, debugger, eager_loading,
|
||||||
with_threads):
|
with_threads, cert):
|
||||||
"""Run a local development server.
|
"""Run a local development server.
|
||||||
|
|
||||||
This server is for development purposes only. It does not provide
|
This server is for development purposes only. It does not provide
|
||||||
|
@ -642,7 +729,7 @@ def run_command(info, host, port, reload, debugger, eager_loading,
|
||||||
|
|
||||||
from werkzeug.serving import run_simple
|
from werkzeug.serving import run_simple
|
||||||
run_simple(host, port, app, use_reloader=reload, use_debugger=debugger,
|
run_simple(host, port, app, use_reloader=reload, use_debugger=debugger,
|
||||||
threaded=with_threads)
|
threaded=with_threads, ssl_context=cert)
|
||||||
|
|
||||||
|
|
||||||
@click.command('shell', short_help='Runs a shell in the app context.')
|
@click.command('shell', short_help='Runs a shell in the app context.')
|
||||||
|
|
|
@ -14,7 +14,9 @@
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
@ -24,8 +26,8 @@ from click.testing import CliRunner
|
||||||
|
|
||||||
from flask import Flask, current_app
|
from flask import Flask, current_app
|
||||||
from flask.cli import (
|
from flask.cli import (
|
||||||
AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv,
|
AppGroup, FlaskGroup, NoAppException, ScriptInfo, dotenv, find_best_app,
|
||||||
find_best_app, get_version, load_dotenv, locate_app, prepare_import,
|
get_version, load_dotenv, locate_app, prepare_import, run_command,
|
||||||
with_appcontext
|
with_appcontext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -464,3 +466,62 @@ def test_dotenv_optional(monkeypatch):
|
||||||
monkeypatch.chdir(test_path)
|
monkeypatch.chdir(test_path)
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
assert 'FOO' not in os.environ
|
assert 'FOO' not in os.environ
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_cert_path():
|
||||||
|
# no key
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--cert', __file__])
|
||||||
|
|
||||||
|
# no cert
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--key', __file__])
|
||||||
|
|
||||||
|
ctx = run_command.make_context(
|
||||||
|
'run', ['--cert', __file__, '--key', __file__])
|
||||||
|
assert ctx.params['cert'] == (__file__, __file__)
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_cert_adhoc(monkeypatch):
|
||||||
|
monkeypatch.setitem(sys.modules, 'OpenSSL', None)
|
||||||
|
|
||||||
|
# pyOpenSSL not installed
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--cert', 'adhoc'])
|
||||||
|
|
||||||
|
# pyOpenSSL installed
|
||||||
|
monkeypatch.setitem(sys.modules, 'OpenSSL', types.ModuleType('OpenSSL'))
|
||||||
|
ctx = run_command.make_context('run', ['--cert', 'adhoc'])
|
||||||
|
assert ctx.params['cert'] == 'adhoc'
|
||||||
|
|
||||||
|
# no key with adhoc
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--cert', 'adhoc', '--key', __file__])
|
||||||
|
|
||||||
|
|
||||||
|
def test_run_cert_import(monkeypatch):
|
||||||
|
monkeypatch.setitem(sys.modules, 'not_here', None)
|
||||||
|
|
||||||
|
# ImportError
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--cert', 'not_here'])
|
||||||
|
|
||||||
|
# not an SSLContext
|
||||||
|
if sys.version_info >= (2, 7):
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context('run', ['--cert', 'flask'])
|
||||||
|
|
||||||
|
# SSLContext
|
||||||
|
if sys.version_info < (2, 7):
|
||||||
|
ssl_context = object()
|
||||||
|
else:
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
|
||||||
|
|
||||||
|
monkeypatch.setitem(sys.modules, 'ssl_context', ssl_context)
|
||||||
|
ctx = run_command.make_context('run', ['--cert', 'ssl_context'])
|
||||||
|
assert ctx.params['cert'] is ssl_context
|
||||||
|
|
||||||
|
# no --key with SSLContext
|
||||||
|
with pytest.raises(click.BadParameter):
|
||||||
|
run_command.make_context(
|
||||||
|
'run', ['--cert', 'ssl_context', '--key', __file__])
|
||||||
|
|
Loading…
Reference in New Issue