Hass.io ingress (#22505)

* Fix API stream of snapshot / Add ingress

* fix lint

* Fix stream handling

* Cleanup api handling

* fix typing

* Set proxy header

* Use header constant

* Enable the ingress setup

* fix lint

* Fix name

* Fix tests

* fix lint

* forward params

* Add tests for ingress

* Cleanup cookie handling with aiohttp 3.5

* Add more tests

* Fix tests

* Fix lint

* Fix header handling for steam

* forward header too

* fix lint

* fix flake
pull/22622/head
Pascal Vizeli 2019-04-01 14:16:16 +02:00 committed by GitHub
parent 42e3e878df
commit 6829ecad9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 458 additions and 88 deletions

View File

@ -20,6 +20,7 @@ from .auth import async_setup_auth
from .discovery import async_setup_discovery
from .handler import HassIO, HassioAPIError
from .http import HassIOView
from .ingress import async_setup_ingress
_LOGGER = logging.getLogger(__name__)
@ -270,4 +271,7 @@ async def async_setup(hass, config):
# Init auth Hass.io feature
async_setup_auth(hass)
# Init ingress Hass.io feature
async_setup_ingress(hass, host)
return True

View File

@ -9,6 +9,7 @@ ATTR_UUID = 'uuid'
ATTR_USERNAME = 'username'
ATTR_PASSWORD = 'password'
X_HASSIO = 'X-HASSIO-KEY'
X_HASS_USER_ID = 'X-HASS-USER-ID'
X_HASS_IS_ADMIN = 'X-HASS-IS-ADMIN'
X_HASSIO = 'X-Hassio-Key'
X_INGRESS_PATH = "X-Ingress-Path"
X_HASS_USER_ID = 'X-Hass-User-ID'
X_HASS_IS_ADMIN = 'X-Hass-Is-Admin'

View File

@ -3,10 +3,11 @@ import asyncio
import logging
import os
import re
from typing import Dict, Union
import aiohttp
from aiohttp import web
from aiohttp.hdrs import CONTENT_TYPE
from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH
from aiohttp.web_exceptions import HTTPBadGateway
import async_timeout
@ -20,7 +21,8 @@ _LOGGER = logging.getLogger(__name__)
NO_TIMEOUT = re.compile(
r'^(?:'
r'|homeassistant/update'
r'|host/update'
r'|hassos/update'
r'|hassos/update/cli'
r'|supervisor/update'
r'|addons/[^/]+/(?:update|install|rebuild)'
r'|snapshots/.+/full'
@ -44,25 +46,26 @@ class HassIOView(HomeAssistantView):
url = "/api/hassio/{path:.+}"
requires_auth = False
def __init__(self, host, websession):
def __init__(self, host: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io base view."""
self._host = host
self._websession = websession
async def _handle(self, request, path):
async def _handle(
self, request: web.Request, path: str
) -> Union[web.Response, web.StreamResponse]:
"""Route data to Hass.io."""
if _need_auth(path) and not request[KEY_AUTHENTICATED]:
return web.Response(status=401)
client = await self._command_proxy(path, request)
data = await client.read()
return _create_response(client, data)
return await self._command_proxy(path, request)
get = _handle
post = _handle
async def _command_proxy(self, path, request):
async def _command_proxy(
self, path: str, request: web.Request
) -> Union[web.Response, web.StreamResponse]:
"""Return a client request with proxy origin for Hass.io supervisor.
This method is a coroutine.
@ -71,29 +74,38 @@ class HassIOView(HomeAssistantView):
hass = request.app['hass']
data = None
headers = {
X_HASSIO: os.environ.get('HASSIO_TOKEN', ""),
}
user = request.get('hass_user')
if user is not None:
headers[X_HASS_USER_ID] = request['hass_user'].id
headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin))
headers = _init_header(request)
try:
with async_timeout.timeout(10, loop=hass.loop):
data = await request.read()
if data:
headers[CONTENT_TYPE] = request.content_type
else:
data = None
method = getattr(self._websession, request.method.lower())
client = await method(
"http://{}/{}".format(self._host, path), data=data,
headers=headers, timeout=read_timeout
)
print(client.headers)
return client
# Simple request
if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000:
# Return Response
body = await client.read()
return web.Response(
content_type=client.content_type,
status=client.status,
body=body,
)
# Stream response
response = web.StreamResponse(status=client.status)
response.content_type = client.content_type
await response.prepare(request)
async for data in client.content.iter_chunked(4096):
await response.write(data)
return response
except aiohttp.ClientError as err:
_LOGGER.error("Client error on api %s request %s", path, err)
@ -104,23 +116,30 @@ class HassIOView(HomeAssistantView):
raise HTTPBadGateway()
def _create_response(client, data):
"""Convert a response from client request."""
return web.Response(
body=data,
status=client.status,
content_type=client.content_type,
)
def _init_header(request: web.Request) -> Dict[str, str]:
"""Create initial header."""
headers = {
X_HASSIO: os.environ.get('HASSIO_TOKEN', ""),
CONTENT_TYPE: request.content_type,
}
# Add user data
user = request.get('hass_user')
if user is not None:
headers[X_HASS_USER_ID] = request['hass_user'].id
headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin))
return headers
def _get_timeout(path):
def _get_timeout(path: str) -> int:
"""Return timeout for a URL path."""
if NO_TIMEOUT.match(path):
return 0
return 300
def _need_auth(path):
def _need_auth(path: str) -> bool:
"""Return if a path need authentication."""
if NO_AUTH.match(path):
return False

View File

@ -0,0 +1,210 @@
"""Hass.io Add-on ingress service."""
import asyncio
from ipaddress import ip_address
import os
from typing import Dict, Union
import aiohttp
from aiohttp import web
from aiohttp import hdrs
from aiohttp.web_exceptions import HTTPBadGateway
from multidict import CIMultiDict
from homeassistant.core import callback
from homeassistant.components.http import HomeAssistantView
from homeassistant.helpers.typing import HomeAssistantType
from .const import X_HASSIO, X_INGRESS_PATH
@callback
def async_setup_ingress(hass: HomeAssistantType, host: str):
"""Auth setup."""
websession = hass.helpers.aiohttp_client.async_get_clientsession()
hassio_ingress = HassIOIngress(host, websession)
hass.http.register_view(hassio_ingress)
class HassIOIngress(HomeAssistantView):
"""Hass.io view to handle base part."""
name = "api:hassio:ingress"
url = "/api/hassio_ingress/{addon}/{path:.+}"
requires_auth = False
def __init__(self, host: str, websession: aiohttp.ClientSession):
"""Initialize a Hass.io ingress view."""
self._host = host
self._websession = websession
def _create_url(self, addon: str, path: str) -> str:
"""Create URL to service."""
return "http://{}/addons/{}/web/{}".format(self._host, addon, path)
async def _handle(
self, request: web.Request, addon: str, path: str
) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]:
"""Route data to Hass.io ingress service."""
try:
# Websocket
if _is_websocket(request):
return await self._handle_websocket(request, addon, path)
# Request
return await self._handle_request(request, addon, path)
except aiohttp.ClientError:
pass
raise HTTPBadGateway() from None
get = _handle
post = _handle
put = _handle
delete = _handle
async def _handle_websocket(
self, request: web.Request, addon: str, path: str
) -> web.WebSocketResponse:
"""Ingress route for websocket."""
ws_server = web.WebSocketResponse()
await ws_server.prepare(request)
url = self._create_url(addon, path)
source_header = _init_header(request, addon)
# Start proxy
async with self._websession.ws_connect(
url, headers=source_header
) as ws_client:
# Proxy requests
await asyncio.wait(
[
_websocket_forward(ws_server, ws_client),
_websocket_forward(ws_client, ws_server),
],
return_when=asyncio.FIRST_COMPLETED
)
return ws_server
async def _handle_request(
self, request: web.Request, addon: str, path: str
) -> Union[web.Response, web.StreamResponse]:
"""Ingress route for request."""
url = self._create_url(addon, path)
data = await request.read()
source_header = _init_header(request, addon)
async with self._websession.request(
request.method, url, headers=source_header,
params=request.query, data=data, cookies=request.cookies
) as result:
headers = _response_header(result)
# Simple request
if hdrs.CONTENT_LENGTH in result.headers and \
int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000:
# Return Response
body = await result.read()
return web.Response(
headers=headers,
status=result.status,
body=body
)
# Stream response
response = web.StreamResponse(
status=result.status, headers=headers)
response.content_type = result.content_type
try:
await response.prepare(request)
async for data in result.content:
await response.write(data)
except (aiohttp.ClientError, aiohttp.ClientPayloadError):
pass
return response
def _init_header(
request: web.Request, addon: str
) -> Union[CIMultiDict, Dict[str, str]]:
"""Create initial header."""
headers = {}
# filter flags
for name, value in request.headers.items():
if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE):
continue
headers[name] = value
# Inject token / cleanup later on Supervisor
headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "")
# Ingress information
headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(addon)
# Set X-Forwarded-For
forward_for = request.headers.get(hdrs.X_FORWARDED_FOR)
connected_ip = ip_address(request.transport.get_extra_info('peername')[0])
if forward_for:
forward_for = "{}, {!s}".format(forward_for, connected_ip)
else:
forward_for = "{!s}".format(connected_ip)
headers[hdrs.X_FORWARDED_FOR] = forward_for
# Set X-Forwarded-Host
forward_host = request.headers.get(hdrs.X_FORWARDED_HOST)
if not forward_host:
forward_host = request.host
headers[hdrs.X_FORWARDED_HOST] = forward_host
# Set X-Forwarded-Proto
forward_proto = request.headers.get(hdrs.X_FORWARDED_PROTO)
if not forward_proto:
forward_proto = request.url.scheme
headers[hdrs.X_FORWARDED_PROTO] = forward_proto
return headers
def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]:
"""Create response header."""
headers = {}
for name, value in response.headers.items():
if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH,
hdrs.CONTENT_TYPE):
continue
headers[name] = value
return headers
def _is_websocket(request: web.Request) -> bool:
"""Return True if request is a websocket."""
headers = request.headers
if headers.get(hdrs.CONNECTION) == "Upgrade" and \
headers.get(hdrs.UPGRADE) == "websocket":
return True
return False
async def _websocket_forward(ws_from, ws_to):
"""Handle websocket message directly."""
async for msg in ws_from:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws_to.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type == aiohttp.WSMsgType.PING:
await ws_to.ping()
elif msg.type == aiohttp.WSMsgType.PONG:
await ws_to.pong()
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra)

View File

@ -1,29 +1,22 @@
"""The tests for the hassio component."""
import asyncio
from unittest.mock import patch, Mock, MagicMock
from unittest.mock import patch
import pytest
from homeassistant.const import HTTP_HEADER_HA_AUTH
from tests.common import mock_coro
from . import API_PASSWORD
@asyncio.coroutine
def test_forward_request(hassio_client):
def test_forward_request(hassio_client, aioclient_mock):
"""Test fetching normal path."""
response = MagicMock()
response.read.return_value = mock_coro('data')
aioclient_mock.post("http://127.0.0.1/beer", text="response")
with patch('homeassistant.components.hassio.HassIOView._command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http'
'._create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.post('/api/hassio/beer', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
resp = yield from hassio_client.post('/api/hassio/beer', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
# Check we got right response
assert resp.status == 200
@ -31,8 +24,7 @@ def test_forward_request(hassio_client):
assert body == 'response'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
assert len(aioclient_mock.mock_calls) == 1
@asyncio.coroutine
@ -55,18 +47,13 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type):
'app/index.html', 'app/hassio-app.html', 'app/index.html',
'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js',
])
def test_forward_request_no_auth_for_panel(hassio_client, build_type):
def test_forward_request_no_auth_for_panel(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
response = MagicMock()
response.read.return_value = mock_coro('data')
aioclient_mock.get(
"http://127.0.0.1/{}".format(build_type), text="response")
with patch('homeassistant.components.hassio.HassIOView._command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http.'
'_create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get(
'/api/hassio/{}'.format(build_type))
resp = yield from hassio_client.get('/api/hassio/{}'.format(build_type))
# Check we got right response
assert resp.status == 200
@ -74,22 +61,16 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type):
assert body == 'response'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
assert len(aioclient_mock.mock_calls) == 1
@asyncio.coroutine
def test_forward_request_no_auth_for_logo(hassio_client):
def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock):
"""Test no auth needed for ."""
response = MagicMock()
response.read.return_value = mock_coro('data')
aioclient_mock.get(
"http://127.0.0.1/addons/bl_b392/logo", text="response")
with patch('homeassistant.components.hassio.HassIOView._command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http.'
'_create_response') as mresp:
mresp.return_value = 'response'
resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo')
resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo')
# Check we got right response
assert resp.status == 200
@ -97,24 +78,18 @@ def test_forward_request_no_auth_for_logo(hassio_client):
assert body == 'response'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
assert len(aioclient_mock.mock_calls) == 1
@asyncio.coroutine
def test_forward_log_request(hassio_client):
def test_forward_log_request(hassio_client, aioclient_mock):
"""Test fetching normal log path doesn't remove ANSI color escape codes."""
response = MagicMock()
response.read.return_value = mock_coro('data')
aioclient_mock.get(
"http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m")
with patch('homeassistant.components.hassio.HassIOView._command_proxy',
Mock(return_value=mock_coro(response))), \
patch('homeassistant.components.hassio.http.'
'_create_response') as mresp:
mresp.return_value = '\033[32mresponse\033[0m'
resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={
HTTP_HEADER_HA_AUTH: API_PASSWORD
})
# Check we got right response
assert resp.status == 200
@ -122,8 +97,7 @@ def test_forward_log_request(hassio_client):
assert body == '\033[32mresponse\033[0m'
# Check we forwarded command
assert len(mresp.mock_calls) == 1
assert mresp.mock_calls[0][1] == (response, 'data')
assert len(aioclient_mock.mock_calls) == 1
@asyncio.coroutine
@ -151,5 +125,5 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user,
assert len(aioclient_mock.mock_calls) == 1
req_headers = aioclient_mock.mock_calls[0][-1]
req_headers['X-HASS-USER-ID'] == hass_admin_user.id
req_headers['X-HASS-IS-ADMIN'] == '1'
req_headers['X-Hass-User-ID'] == hass_admin_user.id
req_headers['X-Hass-Is-Admin'] == '1'

View File

@ -0,0 +1,162 @@
"""The tests for the hassio component."""
from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO
from aiohttp.client_exceptions import WSServerHandshakeError
import pytest
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_get(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.get(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_post(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.post("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.post(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_put(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.put("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.put(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"),
("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5")
])
async def test_ingress_request_delete(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.delete("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]), text="test")
resp = await hassio_client.delete(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we got right response
assert resp.status == 200
body = await resp.text()
assert body == "test"
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]
@pytest.mark.parametrize(
'build_type', [
("a3_vl", "test/beer/ws"), ("core", "ws.php"),
("local", "panel/config/stream"), ("jk_921", "hulk")
])
async def test_ingress_websocket(
hassio_client, build_type, aioclient_mock):
"""Test no auth needed for ."""
aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format(
build_type[0], build_type[1]))
# Ignore error because we can setup a full IO infrastructure
with pytest.raises(WSServerHandshakeError):
await hassio_client.ws_connect(
'/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]),
headers={"X-Test-Header": "beer"}
)
# Check we forwarded command
assert len(aioclient_mock.mock_calls) == 1
assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456"
assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \
"/api/hassio_ingress/{}".format(build_type[0])
assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer"
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST]
assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO]

View File

@ -207,7 +207,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock):
assert result
assert aioclient_mock.call_count == 3
assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456"
assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456"
@asyncio.coroutine

View File

@ -102,7 +102,7 @@ class AiohttpClientMocker:
async def match_request(self, method, url, *, data=None, auth=None,
params=None, headers=None, allow_redirects=None,
timeout=None, json=None):
timeout=None, json=None, cookies=None):
"""Match a request against pre-registered requests."""
data = data or json
url = URL(url)