2016-11-25 21:04:06 +00:00
|
|
|
"""
|
|
|
|
This module provides WSGI application to serve the Home Assistant API.
|
|
|
|
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
|
|
https://home-assistant.io/components/http/
|
|
|
|
"""
|
2017-11-04 19:04:05 +00:00
|
|
|
from ipaddress import ip_network
|
2016-11-25 21:04:06 +00:00
|
|
|
import logging
|
2017-11-04 19:04:05 +00:00
|
|
|
import os
|
2016-11-25 21:04:06 +00:00
|
|
|
import ssl
|
2018-08-13 07:26:20 +00:00
|
|
|
from typing import Optional
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
from aiohttp import web
|
2018-03-09 01:51:49 +00:00
|
|
|
from aiohttp.web_exceptions import HTTPMovedPermanently
|
2017-11-04 19:04:05 +00:00
|
|
|
import voluptuous as vol
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2017-11-04 19:04:05 +00:00
|
|
|
from homeassistant.const import (
|
2018-03-09 01:51:49 +00:00
|
|
|
EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, SERVER_PORT)
|
2016-11-25 21:04:06 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2016-12-18 20:56:07 +00:00
|
|
|
import homeassistant.util as hass_util
|
2016-11-25 21:04:06 +00:00
|
|
|
from homeassistant.util.logging import HideSensitiveDataFilter
|
2018-07-16 08:32:07 +00:00
|
|
|
from homeassistant.util import ssl as ssl_util
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
from .auth import setup_auth
|
|
|
|
from .ban import setup_bans
|
|
|
|
from .cors import setup_cors
|
|
|
|
from .real_ip import setup_real_ip
|
2017-03-30 07:50:53 +00:00
|
|
|
from .static import (
|
2017-11-04 19:04:05 +00:00
|
|
|
CachingFileResponse, CachingStaticResource, staticresource_middleware)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
# Import as alias
|
|
|
|
from .const import KEY_AUTHENTICATED, KEY_REAL_IP # noqa
|
|
|
|
from .view import HomeAssistantView # noqa
|
|
|
|
|
2018-03-17 16:37:53 +00:00
|
|
|
REQUIREMENTS = ['aiohttp_cors==0.7.0']
|
2017-04-01 10:36:35 +00:00
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
DOMAIN = 'http'
|
|
|
|
|
|
|
|
CONF_API_PASSWORD = 'api_password'
|
|
|
|
CONF_SERVER_HOST = 'server_host'
|
|
|
|
CONF_SERVER_PORT = 'server_port'
|
2016-12-18 20:56:07 +00:00
|
|
|
CONF_BASE_URL = 'base_url'
|
2016-11-25 21:04:06 +00:00
|
|
|
CONF_SSL_CERTIFICATE = 'ssl_certificate'
|
2018-06-26 15:44:08 +00:00
|
|
|
CONF_SSL_PEER_CERTIFICATE = 'ssl_peer_certificate'
|
2016-11-25 21:04:06 +00:00
|
|
|
CONF_SSL_KEY = 'ssl_key'
|
|
|
|
CONF_CORS_ORIGINS = 'cors_allowed_origins'
|
|
|
|
CONF_USE_X_FORWARDED_FOR = 'use_x_forwarded_for'
|
2018-06-29 20:27:06 +00:00
|
|
|
CONF_TRUSTED_PROXIES = 'trusted_proxies'
|
2016-11-25 21:04:06 +00:00
|
|
|
CONF_TRUSTED_NETWORKS = 'trusted_networks'
|
|
|
|
CONF_LOGIN_ATTEMPTS_THRESHOLD = 'login_attempts_threshold'
|
|
|
|
CONF_IP_BAN_ENABLED = 'ip_ban_enabled'
|
2018-08-14 06:20:17 +00:00
|
|
|
CONF_SSL_PROFILE = 'ssl_profile'
|
|
|
|
|
|
|
|
SSL_MODERN = 'modern'
|
|
|
|
SSL_INTERMEDIATE = 'intermediate'
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
DEFAULT_SERVER_HOST = '0.0.0.0'
|
|
|
|
DEFAULT_DEVELOPMENT = '0'
|
2018-02-17 09:29:14 +00:00
|
|
|
NO_LOGIN_ATTEMPT_THRESHOLD = -1
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
HTTP_SCHEMA = vol.Schema({
|
2018-02-17 09:29:14 +00:00
|
|
|
vol.Optional(CONF_API_PASSWORD): cv.string,
|
2016-11-25 21:04:06 +00:00
|
|
|
vol.Optional(CONF_SERVER_HOST, default=DEFAULT_SERVER_HOST): cv.string,
|
2017-04-30 05:04:49 +00:00
|
|
|
vol.Optional(CONF_SERVER_PORT, default=SERVER_PORT): cv.port,
|
2016-12-18 20:56:07 +00:00
|
|
|
vol.Optional(CONF_BASE_URL): cv.string,
|
2018-02-17 09:29:14 +00:00
|
|
|
vol.Optional(CONF_SSL_CERTIFICATE): cv.isfile,
|
2018-06-26 15:44:08 +00:00
|
|
|
vol.Optional(CONF_SSL_PEER_CERTIFICATE): cv.isfile,
|
2018-02-17 09:29:14 +00:00
|
|
|
vol.Optional(CONF_SSL_KEY): cv.isfile,
|
2017-04-30 05:04:49 +00:00
|
|
|
vol.Optional(CONF_CORS_ORIGINS, default=[]):
|
|
|
|
vol.All(cv.ensure_list, [cv.string]),
|
2018-08-03 11:52:34 +00:00
|
|
|
vol.Inclusive(CONF_USE_X_FORWARDED_FOR, 'proxy'): cv.boolean,
|
|
|
|
vol.Inclusive(CONF_TRUSTED_PROXIES, 'proxy'):
|
2018-06-29 20:27:06 +00:00
|
|
|
vol.All(cv.ensure_list, [ip_network]),
|
2016-11-25 21:04:06 +00:00
|
|
|
vol.Optional(CONF_TRUSTED_NETWORKS, default=[]):
|
|
|
|
vol.All(cv.ensure_list, [ip_network]),
|
|
|
|
vol.Optional(CONF_LOGIN_ATTEMPTS_THRESHOLD,
|
2018-02-17 09:29:14 +00:00
|
|
|
default=NO_LOGIN_ATTEMPT_THRESHOLD):
|
|
|
|
vol.Any(cv.positive_int, NO_LOGIN_ATTEMPT_THRESHOLD),
|
2018-08-14 06:20:17 +00:00
|
|
|
vol.Optional(CONF_IP_BAN_ENABLED, default=True): cv.boolean,
|
|
|
|
vol.Optional(CONF_SSL_PROFILE, default=SSL_MODERN):
|
|
|
|
vol.In([SSL_INTERMEDIATE, SSL_MODERN]),
|
2016-11-25 21:04:06 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
CONFIG_SCHEMA = vol.Schema({
|
|
|
|
DOMAIN: HTTP_SCHEMA,
|
|
|
|
}, extra=vol.ALLOW_EXTRA)
|
|
|
|
|
|
|
|
|
2018-08-13 07:26:20 +00:00
|
|
|
class ApiConfig:
|
|
|
|
"""Configuration settings for API server."""
|
|
|
|
|
|
|
|
def __init__(self, host: str, port: Optional[int] = SERVER_PORT,
|
|
|
|
use_ssl: bool = False,
|
|
|
|
api_password: Optional[str] = None) -> None:
|
|
|
|
"""Initialize a new API config object."""
|
|
|
|
self.host = host
|
|
|
|
self.port = port
|
|
|
|
self.api_password = api_password
|
|
|
|
|
|
|
|
if host.startswith(("http://", "https://")):
|
|
|
|
self.base_url = host
|
|
|
|
elif use_ssl:
|
|
|
|
self.base_url = "https://{}".format(host)
|
|
|
|
else:
|
|
|
|
self.base_url = "http://{}".format(host)
|
|
|
|
|
|
|
|
if port is not None:
|
|
|
|
self.base_url += ':{}'.format(port)
|
|
|
|
|
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def async_setup(hass, config):
|
2016-11-25 21:04:06 +00:00
|
|
|
"""Set up the HTTP API and debug interface."""
|
|
|
|
conf = config.get(DOMAIN)
|
|
|
|
|
|
|
|
if conf is None:
|
|
|
|
conf = HTTP_SCHEMA({})
|
|
|
|
|
2018-02-17 09:29:14 +00:00
|
|
|
api_password = conf.get(CONF_API_PASSWORD)
|
2016-11-25 21:04:06 +00:00
|
|
|
server_host = conf[CONF_SERVER_HOST]
|
|
|
|
server_port = conf[CONF_SERVER_PORT]
|
2018-02-17 09:29:14 +00:00
|
|
|
ssl_certificate = conf.get(CONF_SSL_CERTIFICATE)
|
2018-06-26 15:44:08 +00:00
|
|
|
ssl_peer_certificate = conf.get(CONF_SSL_PEER_CERTIFICATE)
|
2018-02-17 09:29:14 +00:00
|
|
|
ssl_key = conf.get(CONF_SSL_KEY)
|
2016-11-25 21:04:06 +00:00
|
|
|
cors_origins = conf[CONF_CORS_ORIGINS]
|
2018-08-03 11:52:34 +00:00
|
|
|
use_x_forwarded_for = conf.get(CONF_USE_X_FORWARDED_FOR, False)
|
|
|
|
trusted_proxies = conf.get(CONF_TRUSTED_PROXIES, [])
|
2016-11-25 21:04:06 +00:00
|
|
|
trusted_networks = conf[CONF_TRUSTED_NETWORKS]
|
|
|
|
is_ban_enabled = conf[CONF_IP_BAN_ENABLED]
|
|
|
|
login_threshold = conf[CONF_LOGIN_ATTEMPTS_THRESHOLD]
|
2018-08-14 06:20:17 +00:00
|
|
|
ssl_profile = conf[CONF_SSL_PROFILE]
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
if api_password is not None:
|
|
|
|
logging.getLogger('aiohttp.access').addFilter(
|
|
|
|
HideSensitiveDataFilter(api_password))
|
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
server = HomeAssistantHTTP(
|
2016-11-25 21:04:06 +00:00
|
|
|
hass,
|
|
|
|
server_host=server_host,
|
|
|
|
server_port=server_port,
|
|
|
|
api_password=api_password,
|
|
|
|
ssl_certificate=ssl_certificate,
|
2018-06-26 15:44:08 +00:00
|
|
|
ssl_peer_certificate=ssl_peer_certificate,
|
2016-11-25 21:04:06 +00:00
|
|
|
ssl_key=ssl_key,
|
|
|
|
cors_origins=cors_origins,
|
|
|
|
use_x_forwarded_for=use_x_forwarded_for,
|
2018-06-29 20:27:06 +00:00
|
|
|
trusted_proxies=trusted_proxies,
|
2016-11-25 21:04:06 +00:00
|
|
|
trusted_networks=trusted_networks,
|
|
|
|
login_threshold=login_threshold,
|
2018-08-14 06:20:17 +00:00
|
|
|
is_ban_enabled=is_ban_enabled,
|
|
|
|
ssl_profile=ssl_profile,
|
2016-11-25 21:04:06 +00:00
|
|
|
)
|
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def stop_server(event):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Stop the server."""
|
2018-03-09 01:51:49 +00:00
|
|
|
await server.stop()
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def start_server(event):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Start the server."""
|
2016-11-25 21:04:06 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_server)
|
2018-03-09 01:51:49 +00:00
|
|
|
await server.start()
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_server)
|
|
|
|
|
|
|
|
hass.http = server
|
2016-12-18 20:56:07 +00:00
|
|
|
|
|
|
|
host = conf.get(CONF_BASE_URL)
|
|
|
|
|
|
|
|
if host:
|
2016-12-18 22:59:45 +00:00
|
|
|
port = None
|
2016-12-18 20:56:07 +00:00
|
|
|
elif server_host != DEFAULT_SERVER_HOST:
|
|
|
|
host = server_host
|
2016-12-18 22:59:45 +00:00
|
|
|
port = server_port
|
2016-12-18 20:56:07 +00:00
|
|
|
else:
|
|
|
|
host = hass_util.get_local_ip()
|
2016-12-18 22:59:45 +00:00
|
|
|
port = server_port
|
2016-12-18 20:56:07 +00:00
|
|
|
|
2018-08-13 07:26:20 +00:00
|
|
|
hass.config.api = ApiConfig(host, port, ssl_certificate is not None,
|
|
|
|
api_password)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class HomeAssistantHTTP:
|
2018-02-15 21:06:14 +00:00
|
|
|
"""HTTP server for Home Assistant."""
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-06-26 15:44:08 +00:00
|
|
|
def __init__(self, hass, api_password,
|
|
|
|
ssl_certificate, ssl_peer_certificate,
|
2016-11-25 21:04:06 +00:00
|
|
|
ssl_key, server_host, server_port, cors_origins,
|
2018-06-29 20:27:06 +00:00
|
|
|
use_x_forwarded_for, trusted_proxies, trusted_networks,
|
2018-08-14 06:20:17 +00:00
|
|
|
login_threshold, is_ban_enabled, ssl_profile):
|
2018-02-15 21:06:14 +00:00
|
|
|
"""Initialize the HTTP Home Assistant server."""
|
|
|
|
app = self.app = web.Application(
|
|
|
|
middlewares=[staticresource_middleware])
|
|
|
|
|
|
|
|
# This order matters
|
2018-06-29 20:27:06 +00:00
|
|
|
setup_real_ip(app, use_x_forwarded_for, trusted_proxies)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
if is_ban_enabled:
|
2018-02-15 21:06:14 +00:00
|
|
|
setup_bans(hass, app, login_threshold)
|
|
|
|
|
2018-07-01 02:31:36 +00:00
|
|
|
if hass.auth.active:
|
|
|
|
if hass.auth.support_legacy:
|
|
|
|
_LOGGER.warning("Experimental auth api enabled and "
|
|
|
|
"legacy_api_password support enabled. Please "
|
|
|
|
"use access_token instead api_password, "
|
|
|
|
"although you can still use legacy "
|
|
|
|
"api_password")
|
|
|
|
else:
|
|
|
|
_LOGGER.warning("Experimental auth api enabled. Please use "
|
|
|
|
"access_token instead api_password.")
|
|
|
|
elif api_password is None:
|
|
|
|
_LOGGER.warning("You have been advised to set http.api_password.")
|
|
|
|
|
|
|
|
setup_auth(app, trusted_networks, hass.auth.active,
|
|
|
|
support_legacy=hass.auth.support_legacy,
|
|
|
|
api_password=api_password)
|
2018-02-15 21:06:14 +00:00
|
|
|
|
2018-07-19 06:37:00 +00:00
|
|
|
setup_cors(app, cors_origins)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-02-15 21:06:14 +00:00
|
|
|
app['hass'] = hass
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
self.hass = hass
|
|
|
|
self.api_password = api_password
|
|
|
|
self.ssl_certificate = ssl_certificate
|
2018-06-26 15:44:08 +00:00
|
|
|
self.ssl_peer_certificate = ssl_peer_certificate
|
2016-11-25 21:04:06 +00:00
|
|
|
self.ssl_key = ssl_key
|
|
|
|
self.server_host = server_host
|
|
|
|
self.server_port = server_port
|
2018-08-13 10:40:06 +00:00
|
|
|
self.trusted_networks = trusted_networks
|
2018-02-15 21:06:14 +00:00
|
|
|
self.is_ban_enabled = is_ban_enabled
|
2018-08-14 06:20:17 +00:00
|
|
|
self.ssl_profile = ssl_profile
|
2016-11-25 21:04:06 +00:00
|
|
|
self._handler = None
|
|
|
|
self.server = None
|
|
|
|
|
|
|
|
def register_view(self, view):
|
|
|
|
"""Register a view with the WSGI server.
|
|
|
|
|
|
|
|
The view argument must be a class that inherits from HomeAssistantView.
|
|
|
|
It is optional to instantiate it before registering; this method will
|
|
|
|
handle it either way.
|
|
|
|
"""
|
|
|
|
if isinstance(view, type):
|
|
|
|
# Instantiate the view, if needed
|
|
|
|
view = view()
|
|
|
|
|
|
|
|
if not hasattr(view, 'url'):
|
|
|
|
class_name = view.__class__.__name__
|
|
|
|
raise AttributeError(
|
|
|
|
'{0} missing required attribute "url"'.format(class_name)
|
|
|
|
)
|
|
|
|
|
|
|
|
if not hasattr(view, 'name'):
|
|
|
|
class_name = view.__class__.__name__
|
|
|
|
raise AttributeError(
|
|
|
|
'{0} missing required attribute "name"'.format(class_name)
|
|
|
|
)
|
|
|
|
|
2018-07-19 06:37:00 +00:00
|
|
|
view.register(self.app, self.app.router)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
def register_redirect(self, url, redirect_to):
|
|
|
|
"""Register a redirect with the server.
|
|
|
|
|
|
|
|
If given this must be either a string or callable. In case of a
|
|
|
|
callable it's called with the url adapter that triggered the match and
|
|
|
|
the values of the URL as keyword arguments and has to return the target
|
|
|
|
for the redirect, otherwise it has to be a string with placeholders in
|
|
|
|
rule syntax.
|
|
|
|
"""
|
|
|
|
def redirect(request):
|
|
|
|
"""Redirect to location."""
|
|
|
|
raise HTTPMovedPermanently(redirect_to)
|
|
|
|
|
|
|
|
self.app.router.add_route('GET', url, redirect)
|
|
|
|
|
2017-03-30 07:50:53 +00:00
|
|
|
def register_static_path(self, url_path, path, cache_headers=True):
|
|
|
|
"""Register a folder or file to serve as a static path."""
|
2016-11-25 21:04:06 +00:00
|
|
|
if os.path.isdir(path):
|
2017-03-30 07:50:53 +00:00
|
|
|
if cache_headers:
|
|
|
|
resource = CachingStaticResource
|
|
|
|
else:
|
|
|
|
resource = web.StaticResource
|
|
|
|
self.app.router.register_resource(resource(url_path, path))
|
|
|
|
return
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2017-03-30 07:50:53 +00:00
|
|
|
if cache_headers:
|
2018-03-09 01:51:49 +00:00
|
|
|
async def serve_file(request):
|
2017-03-30 07:50:53 +00:00
|
|
|
"""Serve file from disk."""
|
|
|
|
return CachingFileResponse(path)
|
|
|
|
else:
|
2018-03-09 01:51:49 +00:00
|
|
|
async def serve_file(request):
|
2017-03-30 07:50:53 +00:00
|
|
|
"""Serve file from disk."""
|
|
|
|
return web.FileResponse(path)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
# aiohttp supports regex matching for variables. Using that as temp
|
|
|
|
# to work around cache busting MD5.
|
|
|
|
# Turns something like /static/dev-panel.html into
|
|
|
|
# /static/{filename:dev-panel(-[a-z0-9]{32}|)\.html}
|
2017-03-30 07:50:53 +00:00
|
|
|
base, ext = os.path.splitext(url_path)
|
|
|
|
if ext:
|
|
|
|
base, file = base.rsplit('/', 1)
|
|
|
|
regex = r"{}(-[a-z0-9]{{32}}|){}".format(file, ext)
|
|
|
|
url_pattern = "{}/{{filename:{}}}".format(base, regex)
|
|
|
|
else:
|
|
|
|
url_pattern = url_path
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
self.app.router.add_route('GET', url_pattern, serve_file)
|
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def start(self):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Start the WSGI server."""
|
2018-03-05 21:28:41 +00:00
|
|
|
# We misunderstood the startup signal. You're not allowed to change
|
|
|
|
# anything during startup. Temp workaround.
|
|
|
|
# pylint: disable=protected-access
|
|
|
|
self.app._on_startup.freeze()
|
2018-03-09 01:51:49 +00:00
|
|
|
await self.app.startup()
|
2016-11-25 21:04:06 +00:00
|
|
|
|
|
|
|
if self.ssl_certificate:
|
2016-12-13 06:02:24 +00:00
|
|
|
try:
|
2018-08-14 06:20:17 +00:00
|
|
|
if self.ssl_profile == SSL_INTERMEDIATE:
|
|
|
|
context = ssl_util.server_context_intermediate()
|
|
|
|
else:
|
|
|
|
context = ssl_util.server_context_modern()
|
2016-12-13 06:02:24 +00:00
|
|
|
context.load_cert_chain(self.ssl_certificate, self.ssl_key)
|
|
|
|
except OSError as error:
|
|
|
|
_LOGGER.error("Could not read SSL certificate from %s: %s",
|
|
|
|
self.ssl_certificate, error)
|
|
|
|
return
|
2018-06-26 15:44:08 +00:00
|
|
|
|
|
|
|
if self.ssl_peer_certificate:
|
|
|
|
context.verify_mode = ssl.CERT_REQUIRED
|
|
|
|
context.load_verify_locations(cafile=self.ssl_peer_certificate)
|
|
|
|
|
2016-11-25 21:04:06 +00:00
|
|
|
else:
|
|
|
|
context = None
|
|
|
|
|
2016-11-27 22:01:12 +00:00
|
|
|
# Aiohttp freezes apps after start so that no changes can be made.
|
|
|
|
# However in Home Assistant components can be discovered after boot.
|
|
|
|
# This will now raise a RunTimeError.
|
2018-03-05 21:28:41 +00:00
|
|
|
# To work around this we now prevent the router from getting frozen
|
|
|
|
self.app._router.freeze = lambda: None
|
2016-11-27 22:01:12 +00:00
|
|
|
|
2017-03-30 07:50:53 +00:00
|
|
|
self._handler = self.app.make_handler(loop=self.hass.loop)
|
2016-11-27 22:01:12 +00:00
|
|
|
|
2016-12-13 06:02:24 +00:00
|
|
|
try:
|
2018-03-09 01:51:49 +00:00
|
|
|
self.server = await self.hass.loop.create_server(
|
2016-12-13 06:02:24 +00:00
|
|
|
self._handler, self.server_host, self.server_port, ssl=context)
|
|
|
|
except OSError as error:
|
|
|
|
_LOGGER.error("Failed to create HTTP server at port %d: %s",
|
|
|
|
self.server_port, error)
|
2016-11-25 21:04:06 +00:00
|
|
|
|
2018-03-09 01:51:49 +00:00
|
|
|
async def stop(self):
|
2017-06-08 13:53:12 +00:00
|
|
|
"""Stop the WSGI server."""
|
2016-12-13 16:57:33 +00:00
|
|
|
if self.server:
|
|
|
|
self.server.close()
|
2018-03-09 01:51:49 +00:00
|
|
|
await self.server.wait_closed()
|
|
|
|
await self.app.shutdown()
|
2016-12-13 16:57:33 +00:00
|
|
|
if self._handler:
|
2018-03-09 01:51:49 +00:00
|
|
|
await self._handler.shutdown(10)
|
|
|
|
await self.app.cleanup()
|