mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-23 00:01:36 +00:00
Merge pull request #4147 from mhils/asgi
add ASGI support for embedded apps
This commit is contained in:
commit
5cb403002f
@ -9,6 +9,7 @@ Unreleased: mitmproxy next
|
|||||||
* Prevent transparent mode from connecting to itself in the basic cases (@prinzhorn)
|
* Prevent transparent mode from connecting to itself in the basic cases (@prinzhorn)
|
||||||
* Display HTTP trailers in mitmweb (@sanlengjingvv)
|
* Display HTTP trailers in mitmweb (@sanlengjingvv)
|
||||||
* Revamp onboarding app (@mhils)
|
* Revamp onboarding app (@mhils)
|
||||||
|
* Add ASGI support for embedded apps (@mhils)
|
||||||
|
|
||||||
* --- TODO: add new PRs above this line ---
|
* --- TODO: add new PRs above this line ---
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ instance, we're using the Flask framework (http://flask.pocoo.org/) to expose
|
|||||||
a single simplest-possible page.
|
a single simplest-possible page.
|
||||||
"""
|
"""
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
from mitmproxy.addons import wsgiapp
|
from mitmproxy.addons import asgiapp
|
||||||
|
|
||||||
app = Flask("proxapp")
|
app = Flask("proxapp")
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ def hello_world() -> str:
|
|||||||
addons = [
|
addons = [
|
||||||
# Host app at the magic domain "example.com" on port 80. Requests to this
|
# Host app at the magic domain "example.com" on port 80. Requests to this
|
||||||
# domain and port combination will now be routed to the WSGI app instance.
|
# domain and port combination will now be routed to the WSGI app instance.
|
||||||
wsgiapp.WSGIApp(app, "example.com", 80)
|
asgiapp.WSGIApp(app, "example.com", 80)
|
||||||
# SSL works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design.
|
# SSL works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design.
|
||||||
# mitmproxy will connect to said domain and use serve its certificate (unless --no-upstream-cert is set)
|
# mitmproxy will connect to said domain and use serve its certificate (unless --no-upstream-cert is set)
|
||||||
# but won't send any data.
|
# but won't send any data.
|
||||||
|
129
mitmproxy/addons/asgiapp.py
Normal file
129
mitmproxy/addons/asgiapp.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import asyncio
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
import asgiref.compatibility
|
||||||
|
import asgiref.wsgi
|
||||||
|
|
||||||
|
from mitmproxy import ctx, http
|
||||||
|
|
||||||
|
|
||||||
|
class ASGIApp:
|
||||||
|
"""
|
||||||
|
An addon that hosts an ASGI/WSGI HTTP app within mitmproxy, at a specified hostname and port.
|
||||||
|
|
||||||
|
Some important caveats:
|
||||||
|
- This implementation will block and wait until the entire HTTP response is completed before sending out data.
|
||||||
|
- It currently only implements the HTTP protocol (Lifespan and WebSocket are unimplemented).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, asgi_app, host: str, port: int):
|
||||||
|
asgi_app = asgiref.compatibility.guarantee_single_callable(asgi_app)
|
||||||
|
self.asgi_app, self.host, self.port = asgi_app, host, port
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return f"asgiapp:{self.host}:{self.port}"
|
||||||
|
|
||||||
|
def request(self, flow: http.HTTPFlow) -> None:
|
||||||
|
assert flow.reply
|
||||||
|
if (flow.request.pretty_host, flow.request.port) == (self.host, self.port) and not flow.reply.has_message:
|
||||||
|
flow.reply.take() # pause hook completion
|
||||||
|
asyncio.ensure_future(serve(self.asgi_app, flow))
|
||||||
|
|
||||||
|
|
||||||
|
class WSGIApp(ASGIApp):
|
||||||
|
def __init__(self, wsgi_app, host: str, port: int):
|
||||||
|
asgi_app = asgiref.wsgi.WsgiToAsgi(wsgi_app)
|
||||||
|
super().__init__(asgi_app, host, port)
|
||||||
|
|
||||||
|
|
||||||
|
HTTP_VERSION_MAP = {
|
||||||
|
"HTTP/1.0": "1.0",
|
||||||
|
"HTTP/1.1": "1.1",
|
||||||
|
"HTTP/2.0": "2",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def make_scope(flow: http.HTTPFlow) -> dict:
|
||||||
|
# %3F is a quoted question mark
|
||||||
|
quoted_path = urllib.parse.quote_from_bytes(flow.request.data.path).split("%3F", maxsplit=1)
|
||||||
|
|
||||||
|
# (Unicode string) – HTTP request target excluding any query string, with percent-encoded
|
||||||
|
# sequences and UTF-8 byte sequences decoded into characters.
|
||||||
|
path = quoted_path[0]
|
||||||
|
|
||||||
|
# (byte string) – URL portion after the ?, percent-encoded.
|
||||||
|
query_string: bytes
|
||||||
|
if len(quoted_path) > 1:
|
||||||
|
query_string = quoted_path[1].encode()
|
||||||
|
else:
|
||||||
|
query_string = b""
|
||||||
|
|
||||||
|
return {
|
||||||
|
"type": "http",
|
||||||
|
"asgi": {
|
||||||
|
"version": "3.0",
|
||||||
|
"spec_version": "2.1",
|
||||||
|
},
|
||||||
|
"http_version": HTTP_VERSION_MAP.get(flow.request.http_version, "1.1"),
|
||||||
|
"method": flow.request.method,
|
||||||
|
"scheme": flow.request.scheme,
|
||||||
|
"path": path,
|
||||||
|
"raw_path": flow.request.path,
|
||||||
|
"query_string": query_string,
|
||||||
|
"headers": list(list(x) for x in flow.request.headers.fields),
|
||||||
|
"client": flow.client_conn.address,
|
||||||
|
"extensions": {
|
||||||
|
"mitmproxy.master": ctx.master,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def serve(app, flow: http.HTTPFlow):
|
||||||
|
"""
|
||||||
|
Serves app on flow.
|
||||||
|
"""
|
||||||
|
assert flow.reply
|
||||||
|
|
||||||
|
scope = make_scope(flow)
|
||||||
|
done = asyncio.Event()
|
||||||
|
received_body = False
|
||||||
|
|
||||||
|
async def receive():
|
||||||
|
nonlocal received_body
|
||||||
|
if not received_body:
|
||||||
|
received_body = True
|
||||||
|
return {
|
||||||
|
"type": "http.request",
|
||||||
|
"body": flow.request.raw_content,
|
||||||
|
}
|
||||||
|
else: # pragma: no cover
|
||||||
|
# We really don't expect this to be called a second time, but what to do?
|
||||||
|
# We just wait until the request is done before we continue here with sending a disconnect.
|
||||||
|
await done.wait()
|
||||||
|
return {
|
||||||
|
"type": "http.disconnect"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def send(event):
|
||||||
|
if event["type"] == "http.response.start":
|
||||||
|
flow.response = http.HTTPResponse.make(event["status"], b"", event.get("headers", []))
|
||||||
|
flow.response.decode()
|
||||||
|
elif event["type"] == "http.response.body":
|
||||||
|
flow.response.content += event.get("body", b"")
|
||||||
|
if not event.get("more_body", False):
|
||||||
|
flow.reply.ack()
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"Unexpected event: {event['type']}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
await app(scope, receive, send)
|
||||||
|
if not flow.reply.has_message:
|
||||||
|
raise RuntimeError(f"no response sent.")
|
||||||
|
except Exception as e:
|
||||||
|
ctx.log.error(f"Error in asgi app: {e}")
|
||||||
|
flow.response = http.HTTPResponse.make(500, b"ASGI Error.")
|
||||||
|
flow.reply.ack(force=True)
|
||||||
|
finally:
|
||||||
|
flow.reply.commit()
|
||||||
|
done.set()
|
@ -1,4 +1,4 @@
|
|||||||
from mitmproxy.addons import wsgiapp
|
from mitmproxy.addons import asgiapp
|
||||||
from mitmproxy.addons.onboardingapp import app
|
from mitmproxy.addons.onboardingapp import app
|
||||||
from mitmproxy import ctx
|
from mitmproxy import ctx
|
||||||
|
|
||||||
@ -6,11 +6,11 @@ APP_HOST = "mitm.it"
|
|||||||
APP_PORT = 80
|
APP_PORT = 80
|
||||||
|
|
||||||
|
|
||||||
class Onboarding(wsgiapp.WSGIApp):
|
class Onboarding(asgiapp.WSGIApp):
|
||||||
name = "onboarding"
|
name = "onboarding"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(app, None, None)
|
super().__init__(app, APP_HOST, APP_PORT)
|
||||||
|
|
||||||
def load(self, loader):
|
def load(self, loader):
|
||||||
loader.add_option(
|
loader.add_option(
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
from mitmproxy import ctx
|
|
||||||
from mitmproxy import exceptions
|
|
||||||
|
|
||||||
from mitmproxy.net import wsgi
|
|
||||||
from mitmproxy import version
|
|
||||||
|
|
||||||
|
|
||||||
class WSGIApp:
|
|
||||||
"""
|
|
||||||
An addon that hosts a WSGI app within mitproxy, at a specified
|
|
||||||
hostname and port.
|
|
||||||
"""
|
|
||||||
def __init__(self, app, host, port):
|
|
||||||
self.app, self.host, self.port = app, host, port
|
|
||||||
|
|
||||||
@property
|
|
||||||
def name(self):
|
|
||||||
return "wsgiapp:%s:%s" % (self.host, self.port)
|
|
||||||
|
|
||||||
def serve(self, app, flow):
|
|
||||||
"""
|
|
||||||
Serves app on flow, and prevents further handling of the flow.
|
|
||||||
"""
|
|
||||||
app = wsgi.WSGIAdaptor(
|
|
||||||
app,
|
|
||||||
flow.request.pretty_host,
|
|
||||||
flow.request.port,
|
|
||||||
version.MITMPROXY
|
|
||||||
)
|
|
||||||
err = app.serve(
|
|
||||||
flow,
|
|
||||||
flow.client_conn.wfile,
|
|
||||||
**{"mitmproxy.master": ctx.master}
|
|
||||||
)
|
|
||||||
if err:
|
|
||||||
ctx.log.error("Error in wsgi app. %s" % err)
|
|
||||||
raise exceptions.AddonHalt()
|
|
||||||
flow.reply.kill()
|
|
||||||
|
|
||||||
def request(self, f):
|
|
||||||
if (f.request.pretty_host, f.request.port) == (self.host, self.port):
|
|
||||||
self.serve(self.app, f)
|
|
@ -1,165 +0,0 @@
|
|||||||
import time
|
|
||||||
import traceback
|
|
||||||
import urllib
|
|
||||||
import io
|
|
||||||
|
|
||||||
from mitmproxy.net import http
|
|
||||||
from mitmproxy.utils import strutils
|
|
||||||
|
|
||||||
|
|
||||||
class ClientConn:
|
|
||||||
|
|
||||||
def __init__(self, address):
|
|
||||||
self.address = address
|
|
||||||
|
|
||||||
|
|
||||||
class Flow:
|
|
||||||
|
|
||||||
def __init__(self, address, request):
|
|
||||||
self.client_conn = ClientConn(address)
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
|
||||||
|
|
||||||
def __init__(self, scheme, method, path, http_version, headers, content):
|
|
||||||
self.scheme, self.method, self.path = scheme, method, path
|
|
||||||
self.headers, self.content = headers, content
|
|
||||||
self.http_version = http_version
|
|
||||||
|
|
||||||
|
|
||||||
def date_time_string():
|
|
||||||
"""Return the current date and time formatted for a message header."""
|
|
||||||
WEEKS = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
|
||||||
MONTHS = [
|
|
||||||
None,
|
|
||||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
||||||
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
|
||||||
]
|
|
||||||
now = time.time()
|
|
||||||
year, month, day, hh, mm, ss, wd, y_, z_ = time.gmtime(now)
|
|
||||||
s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % (
|
|
||||||
WEEKS[wd],
|
|
||||||
day, MONTHS[month], year,
|
|
||||||
hh, mm, ss
|
|
||||||
)
|
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
class WSGIAdaptor:
|
|
||||||
|
|
||||||
def __init__(self, app, domain, port, sversion):
|
|
||||||
self.app, self.domain, self.port, self.sversion = app, domain, port, sversion
|
|
||||||
|
|
||||||
def make_environ(self, flow, errsoc, **extra):
|
|
||||||
"""
|
|
||||||
Raises:
|
|
||||||
ValueError, if the content-encoding is invalid.
|
|
||||||
"""
|
|
||||||
path = strutils.always_str(flow.request.path, "latin-1")
|
|
||||||
if '?' in path:
|
|
||||||
path_info, query = strutils.always_str(path, "latin-1").split('?', 1)
|
|
||||||
else:
|
|
||||||
path_info = path
|
|
||||||
query = ''
|
|
||||||
environ = {
|
|
||||||
'wsgi.version': (1, 0),
|
|
||||||
'wsgi.url_scheme': strutils.always_str(flow.request.scheme, "latin-1"),
|
|
||||||
'wsgi.input': io.BytesIO(flow.request.content or b""),
|
|
||||||
'wsgi.errors': errsoc,
|
|
||||||
'wsgi.multithread': True,
|
|
||||||
'wsgi.multiprocess': False,
|
|
||||||
'wsgi.run_once': False,
|
|
||||||
'SERVER_SOFTWARE': self.sversion,
|
|
||||||
'REQUEST_METHOD': strutils.always_str(flow.request.method, "latin-1"),
|
|
||||||
'SCRIPT_NAME': '',
|
|
||||||
'PATH_INFO': urllib.parse.unquote(path_info),
|
|
||||||
'QUERY_STRING': query,
|
|
||||||
'CONTENT_TYPE': strutils.always_str(flow.request.headers.get('Content-Type', ''), "latin-1"),
|
|
||||||
'CONTENT_LENGTH': strutils.always_str(flow.request.headers.get('Content-Length', ''), "latin-1"),
|
|
||||||
'SERVER_NAME': self.domain,
|
|
||||||
'SERVER_PORT': str(self.port),
|
|
||||||
'SERVER_PROTOCOL': strutils.always_str(flow.request.http_version, "latin-1"),
|
|
||||||
}
|
|
||||||
environ.update(extra)
|
|
||||||
if flow.client_conn.address:
|
|
||||||
environ["REMOTE_ADDR"] = strutils.always_str(flow.client_conn.address[0], "latin-1")
|
|
||||||
environ["REMOTE_PORT"] = flow.client_conn.address[1]
|
|
||||||
|
|
||||||
for key, value in flow.request.headers.items():
|
|
||||||
key = 'HTTP_' + strutils.always_str(key, "latin-1").upper().replace('-', '_')
|
|
||||||
if key not in ('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'):
|
|
||||||
environ[key] = value
|
|
||||||
return environ
|
|
||||||
|
|
||||||
def error_page(self, soc, headers_sent, s):
|
|
||||||
"""
|
|
||||||
Make a best-effort attempt to write an error page. If headers are
|
|
||||||
already sent, we just bung the error into the page.
|
|
||||||
"""
|
|
||||||
c = """
|
|
||||||
<html>
|
|
||||||
<h1>Internal Server Error</h1>
|
|
||||||
<pre>{err}"</pre>
|
|
||||||
</html>
|
|
||||||
""".format(err=s).strip().encode()
|
|
||||||
|
|
||||||
if not headers_sent:
|
|
||||||
soc.write(b"HTTP/1.1 500 Internal Server Error\r\n")
|
|
||||||
soc.write(b"Content-Type: text/html\r\n")
|
|
||||||
soc.write("Content-Length: {length}\r\n".format(length=len(c)).encode())
|
|
||||||
soc.write(b"\r\n")
|
|
||||||
soc.write(c)
|
|
||||||
|
|
||||||
def serve(self, request, soc, **env):
|
|
||||||
state = dict(
|
|
||||||
response_started=False,
|
|
||||||
headers_sent=False,
|
|
||||||
status=None,
|
|
||||||
headers=None
|
|
||||||
)
|
|
||||||
|
|
||||||
def write(data):
|
|
||||||
if not state["headers_sent"]:
|
|
||||||
soc.write("HTTP/1.1 {status}\r\n".format(status=state["status"]).encode())
|
|
||||||
headers = state["headers"]
|
|
||||||
if 'server' not in headers:
|
|
||||||
headers["Server"] = self.sversion
|
|
||||||
if 'date' not in headers:
|
|
||||||
headers["Date"] = date_time_string()
|
|
||||||
soc.write(bytes(headers))
|
|
||||||
soc.write(b"\r\n")
|
|
||||||
state["headers_sent"] = True
|
|
||||||
if data:
|
|
||||||
soc.write(data)
|
|
||||||
soc.flush()
|
|
||||||
|
|
||||||
def start_response(status, headers, exc_info=None):
|
|
||||||
if exc_info:
|
|
||||||
if state["headers_sent"]:
|
|
||||||
raise exc_info[1]
|
|
||||||
elif state["status"]:
|
|
||||||
raise AssertionError('Response already started')
|
|
||||||
state["status"] = status
|
|
||||||
state["headers"] = http.Headers([[strutils.always_bytes(k), strutils.always_bytes(v)] for k, v in headers])
|
|
||||||
if exc_info:
|
|
||||||
self.error_page(soc, state["headers_sent"], traceback.format_tb(exc_info[2]))
|
|
||||||
state["headers_sent"] = True
|
|
||||||
|
|
||||||
errs = io.BytesIO()
|
|
||||||
try:
|
|
||||||
dataiter = self.app(
|
|
||||||
self.make_environ(request, errs, **env), start_response
|
|
||||||
)
|
|
||||||
for i in dataiter:
|
|
||||||
write(i)
|
|
||||||
if not state["headers_sent"]:
|
|
||||||
write(b"")
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
s = traceback.format_exc()
|
|
||||||
errs.write(s.encode("utf-8", "replace"))
|
|
||||||
self.error_page(soc, state["headers_sent"], s)
|
|
||||||
except Exception: # pragma: no cover
|
|
||||||
pass
|
|
||||||
return errs.getvalue()
|
|
1
setup.py
1
setup.py
@ -61,6 +61,7 @@ setup(
|
|||||||
# https://packaging.python.org/en/latest/requirements/#install-requires
|
# https://packaging.python.org/en/latest/requirements/#install-requires
|
||||||
# It is not considered best practice to use install_requires to pin dependencies to specific versions.
|
# It is not considered best practice to use install_requires to pin dependencies to specific versions.
|
||||||
install_requires=[
|
install_requires=[
|
||||||
|
"asgiref>=3.2.10, <3.3",
|
||||||
"blinker>=1.4, <1.5",
|
"blinker>=1.4, <1.5",
|
||||||
"Brotli>=1.0,<1.1",
|
"Brotli>=1.0,<1.1",
|
||||||
"certifi>=2019.9.11", # no semver here - this should always be on the last release!
|
"certifi>=2019.9.11", # no semver here - this should always be on the last release!
|
||||||
|
53
test/mitmproxy/addons/test_asgiapp.py
Normal file
53
test/mitmproxy/addons/test_asgiapp.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import flask
|
||||||
|
|
||||||
|
from .. import tservers
|
||||||
|
from mitmproxy.addons import asgiapp
|
||||||
|
|
||||||
|
tapp = flask.Flask(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@tapp.route("/")
|
||||||
|
def hello():
|
||||||
|
return "testapp"
|
||||||
|
|
||||||
|
|
||||||
|
@tapp.route("/error")
|
||||||
|
def error():
|
||||||
|
raise ValueError("An exception...")
|
||||||
|
|
||||||
|
|
||||||
|
async def errapp(scope, receive, send):
|
||||||
|
raise ValueError("errapp")
|
||||||
|
|
||||||
|
|
||||||
|
async def noresponseapp(scope, receive, send):
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class TestApp(tservers.HTTPProxyTest):
|
||||||
|
def addons(self):
|
||||||
|
return [
|
||||||
|
asgiapp.WSGIApp(tapp, "testapp", 80),
|
||||||
|
asgiapp.ASGIApp(errapp, "errapp", 80),
|
||||||
|
asgiapp.ASGIApp(noresponseapp, "noresponseapp", 80),
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_simple(self):
|
||||||
|
p = self.pathoc()
|
||||||
|
with p.connect():
|
||||||
|
ret = p.request("get:'http://testapp/'")
|
||||||
|
assert b"testapp" in ret.content
|
||||||
|
|
||||||
|
def test_app_err(self):
|
||||||
|
p = self.pathoc()
|
||||||
|
with p.connect():
|
||||||
|
ret = p.request("get:'http://errapp/?foo=bar'")
|
||||||
|
assert ret.status_code == 500
|
||||||
|
assert b"ASGI Error" in ret.content
|
||||||
|
|
||||||
|
def test_app_no_response(self):
|
||||||
|
p = self.pathoc()
|
||||||
|
with p.connect():
|
||||||
|
ret = p.request("get:'http://noresponseapp/'")
|
||||||
|
assert ret.status_code == 500
|
||||||
|
assert b"ASGI Error" in ret.content
|
@ -1,41 +0,0 @@
|
|||||||
import flask
|
|
||||||
|
|
||||||
from .. import tservers
|
|
||||||
from mitmproxy.addons import wsgiapp
|
|
||||||
|
|
||||||
tapp = flask.Flask(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@tapp.route("/")
|
|
||||||
def hello():
|
|
||||||
return "testapp"
|
|
||||||
|
|
||||||
|
|
||||||
@tapp.route("/error")
|
|
||||||
def error():
|
|
||||||
raise ValueError("An exception...")
|
|
||||||
|
|
||||||
|
|
||||||
def errapp(environ, start_response):
|
|
||||||
raise ValueError("errapp")
|
|
||||||
|
|
||||||
|
|
||||||
class TestApp(tservers.HTTPProxyTest):
|
|
||||||
def addons(self):
|
|
||||||
return [
|
|
||||||
wsgiapp.WSGIApp(tapp, "testapp", 80),
|
|
||||||
wsgiapp.WSGIApp(errapp, "errapp", 80)
|
|
||||||
]
|
|
||||||
|
|
||||||
def test_simple(self):
|
|
||||||
p = self.pathoc()
|
|
||||||
with p.connect():
|
|
||||||
ret = p.request("get:'http://testapp/'")
|
|
||||||
assert ret.status_code == 200
|
|
||||||
|
|
||||||
def test_app_err(self):
|
|
||||||
p = self.pathoc()
|
|
||||||
with p.connect():
|
|
||||||
ret = p.request("get:'http://errapp/'")
|
|
||||||
assert ret.status_code == 500
|
|
||||||
assert b"ValueError" in ret.content
|
|
@ -1,106 +0,0 @@
|
|||||||
from io import BytesIO
|
|
||||||
import sys
|
|
||||||
from mitmproxy.net import wsgi
|
|
||||||
from mitmproxy.net.http import Headers
|
|
||||||
|
|
||||||
|
|
||||||
def tflow():
|
|
||||||
headers = Headers(test=b"value")
|
|
||||||
req = wsgi.Request("http", "GET", "/", "HTTP/1.1", headers, "")
|
|
||||||
return wsgi.Flow(("127.0.0.1", 8888), req)
|
|
||||||
|
|
||||||
|
|
||||||
class ExampleApp:
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.called = False
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
self.called = True
|
|
||||||
status = '200 OK'
|
|
||||||
response_headers = [('Content-type', 'text/plain')]
|
|
||||||
start_response(status, response_headers)
|
|
||||||
return [b'Hello', b' world!\n']
|
|
||||||
|
|
||||||
|
|
||||||
class TestWSGI:
|
|
||||||
|
|
||||||
def test_make_environ(self):
|
|
||||||
w = wsgi.WSGIAdaptor(None, "foo", 80, "version")
|
|
||||||
tf = tflow()
|
|
||||||
assert w.make_environ(tf, None)
|
|
||||||
|
|
||||||
tf.request.path = "/foo?bar=voing"
|
|
||||||
r = w.make_environ(tf, None)
|
|
||||||
assert r["QUERY_STRING"] == "bar=voing"
|
|
||||||
|
|
||||||
def test_serve(self):
|
|
||||||
ta = ExampleApp()
|
|
||||||
w = wsgi.WSGIAdaptor(ta, "foo", 80, "version")
|
|
||||||
f = tflow()
|
|
||||||
f.request.host = "foo"
|
|
||||||
f.request.port = 80
|
|
||||||
|
|
||||||
wfile = BytesIO()
|
|
||||||
err = w.serve(f, wfile)
|
|
||||||
assert ta.called
|
|
||||||
assert not err
|
|
||||||
|
|
||||||
val = wfile.getvalue()
|
|
||||||
assert b"Hello world" in val
|
|
||||||
assert b"Server:" in val
|
|
||||||
|
|
||||||
def _serve(self, app):
|
|
||||||
w = wsgi.WSGIAdaptor(app, "foo", 80, "version")
|
|
||||||
f = tflow()
|
|
||||||
f.request.host = "foo"
|
|
||||||
f.request.port = 80
|
|
||||||
wfile = BytesIO()
|
|
||||||
w.serve(f, wfile)
|
|
||||||
return wfile.getvalue()
|
|
||||||
|
|
||||||
def test_serve_empty_body(self):
|
|
||||||
def app(environ, start_response):
|
|
||||||
status = '200 OK'
|
|
||||||
response_headers = [('Foo', 'bar')]
|
|
||||||
start_response(status, response_headers)
|
|
||||||
return []
|
|
||||||
assert self._serve(app)
|
|
||||||
|
|
||||||
def test_serve_double_start(self):
|
|
||||||
def app(environ, start_response):
|
|
||||||
try:
|
|
||||||
raise ValueError("foo")
|
|
||||||
except:
|
|
||||||
sys.exc_info()
|
|
||||||
status = '200 OK'
|
|
||||||
response_headers = [('Content-type', 'text/plain')]
|
|
||||||
start_response(status, response_headers)
|
|
||||||
start_response(status, response_headers)
|
|
||||||
assert b"Internal Server Error" in self._serve(app)
|
|
||||||
|
|
||||||
def test_serve_single_err(self):
|
|
||||||
def app(environ, start_response):
|
|
||||||
try:
|
|
||||||
raise ValueError("foo")
|
|
||||||
except:
|
|
||||||
ei = sys.exc_info()
|
|
||||||
status = '200 OK'
|
|
||||||
response_headers = [('Content-type', 'text/plain')]
|
|
||||||
start_response(status, response_headers, ei)
|
|
||||||
yield b""
|
|
||||||
assert b"Internal Server Error" in self._serve(app)
|
|
||||||
|
|
||||||
def test_serve_double_err(self):
|
|
||||||
def app(environ, start_response):
|
|
||||||
try:
|
|
||||||
raise ValueError("foo")
|
|
||||||
except:
|
|
||||||
ei = sys.exc_info()
|
|
||||||
status = '200 OK'
|
|
||||||
response_headers = [('Content-type', 'text/plain')]
|
|
||||||
start_response(status, response_headers)
|
|
||||||
yield b"aaa"
|
|
||||||
start_response(status, response_headers, ei)
|
|
||||||
yield b"bbb"
|
|
||||||
assert b"Internal Server Error" in self._serve(app)
|
|
Loading…
Reference in New Issue
Block a user