cavisson-pythonagent 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cavisson_pythonagent-0.0.1.dist-info/METADATA +32 -0
- cavisson_pythonagent-0.0.1.dist-info/RECORD +180 -0
- cavisson_pythonagent-0.0.1.dist-info/WHEEL +5 -0
- cavisson_pythonagent-0.0.1.dist-info/licenses/LICENSE +19 -0
- cavisson_pythonagent-0.0.1.dist-info/top_level.txt +1 -0
- pythonagent/__init__.py +22 -0
- pythonagent/agent/__init__.py +219 -0
- pythonagent/agent/internal/__init__.py +1 -0
- pythonagent/agent/internal/agent.py +1651 -0
- pythonagent/agent/internal/framesinfo.py +152 -0
- pythonagent/agent/internal/heap_dump.py +39 -0
- pythonagent/agent/internal/intercept_module.py +70 -0
- pythonagent/agent/internal/logs.py +122 -0
- pythonagent/agent/internal/metadata/__init__.py +0 -0
- pythonagent/agent/internal/metadata/agent_meta_data.py +276 -0
- pythonagent/agent/internal/proc_compat.py +75 -0
- pythonagent/agent/internal/profile.py +47 -0
- pythonagent/agent/internal/provider.py +83 -0
- pythonagent/agent/internal/thread_dump.py +85 -0
- pythonagent/agent/internal/udp.py +245 -0
- pythonagent/agent/internal/udp_message.py +800 -0
- pythonagent/agent/probes/Instrumentation/__init__.py +340 -0
- pythonagent/agent/probes/Instrumentation/find.py +243 -0
- pythonagent/agent/probes/Instrumentation/module_version_resolver.py +155 -0
- pythonagent/agent/probes/Instrumentation/new_parser.py +64 -0
- pythonagent/agent/probes/Instrumentation/parser.py +129 -0
- pythonagent/agent/probes/__init__.py +219 -0
- pythonagent/agent/probes/base.py +303 -0
- pythonagent/agent/probes/cache/__init__.py +51 -0
- pythonagent/agent/probes/cache/redis.py +119 -0
- pythonagent/agent/probes/cache/redis_asyncio.py +83 -0
- pythonagent/agent/probes/coroutines/__init__.py +1 -0
- pythonagent/agent/probes/coroutines/asyncio.py +63 -0
- pythonagent/agent/probes/elasticdb/__init__.py +7 -0
- pythonagent/agent/probes/elasticdb/aelastic.py +54 -0
- pythonagent/agent/probes/frameworks/__init__.py +27 -0
- pythonagent/agent/probes/frameworks/agentprofiler.py +105 -0
- pythonagent/agent/probes/frameworks/aiohttp_web.py +155 -0
- pythonagent/agent/probes/frameworks/aisess.py +151 -0
- pythonagent/agent/probes/frameworks/asgi.py +340 -0
- pythonagent/agent/probes/frameworks/bottle.py +27 -0
- pythonagent/agent/probes/frameworks/cherry.py +25 -0
- pythonagent/agent/probes/frameworks/django.py +128 -0
- pythonagent/agent/probes/frameworks/falcon.py +21 -0
- pythonagent/agent/probes/frameworks/fastapi.py +35 -0
- pythonagent/agent/probes/frameworks/flask.py +30 -0
- pythonagent/agent/probes/frameworks/pyramid.py +56 -0
- pythonagent/agent/probes/frameworks/test.py +108 -0
- pythonagent/agent/probes/frameworks/tornado_async_web.py +117 -0
- pythonagent/agent/probes/frameworks/tornado_web.py +133 -0
- pythonagent/agent/probes/frameworks/wsgi.py +353 -0
- pythonagent/agent/probes/grpc/__init__.py +76 -0
- pythonagent/agent/probes/grpc/client_interceptor.py +132 -0
- pythonagent/agent/probes/grpc/server_interceptor.py +129 -0
- pythonagent/agent/probes/havoc/__init__.py +0 -0
- pythonagent/agent/probes/havoc/custom_memory_stress.py +186 -0
- pythonagent/agent/probes/havoc/custom_thread_stress.py +187 -0
- pythonagent/agent/probes/havoc/havoc_constants.py +218 -0
- pythonagent/agent/probes/havoc/havoc_manager.py +981 -0
- pythonagent/agent/probes/http/__init__.py +49 -0
- pythonagent/agent/probes/http/aiohttp_client.py +59 -0
- pythonagent/agent/probes/http/boto.py +12 -0
- pythonagent/agent/probes/http/httplib.py +110 -0
- pythonagent/agent/probes/http/httpx_client.py +116 -0
- pythonagent/agent/probes/http/requests.py +15 -0
- pythonagent/agent/probes/http/tornado_httpclient.py +85 -0
- pythonagent/agent/probes/http/urllib3.py +16 -0
- pythonagent/agent/probes/langchain/__init__.py +21 -0
- pythonagent/agent/probes/langchain/base_tool.py +95 -0
- pythonagent/agent/probes/langchain/langchain_community.py +136 -0
- pythonagent/agent/probes/langchain/langchain_core.py +32 -0
- pythonagent/agent/probes/langchain/langchain_openai.py +110 -0
- pythonagent/agent/probes/logging/__init__.py +106 -0
- pythonagent/agent/probes/message_brokers/__init__.py +4 -0
- pythonagent/agent/probes/message_brokers/pika.py +126 -0
- pythonagent/agent/probes/mongodb/__init__.py +6 -0
- pythonagent/agent/probes/mongodb/pymongo.py +286 -0
- pythonagent/agent/probes/openai/__init__.py +3 -0
- pythonagent/agent/probes/openai/openai.py +797 -0
- pythonagent/agent/probes/span.py +101 -0
- pythonagent/agent/probes/sql/__init__.py +13 -0
- pythonagent/agent/probes/sql/botocores3.py +51 -0
- pythonagent/agent/probes/sql/dbapi.py +285 -0
- pythonagent/agent/probes/sql/dynamodb.py +90 -0
- pythonagent/agent/probes/sql/mysql_connector.py +24 -0
- pythonagent/agent/probes/sql/mysql_connector_cext.py +24 -0
- pythonagent/agent/probes/sql/mysqldb.py +43 -0
- pythonagent/agent/probes/sql/psycopg2.py +174 -0
- pythonagent/agent/probes/sql/pymysql.py +25 -0
- pythonagent/bootstrap/__init__.py +0 -0
- pythonagent/bootstrap/cav_gunicorn.py +26 -0
- pythonagent/bootstrap/cavagent_lambda_wrapper.py +291 -0
- pythonagent/bootstrap/run.py +47 -0
- pythonagent/bootstrap/sitecustomize.py +287 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/instrumentationprofile.json +26 -0
- pythonagent/cavisson/netdiagnostics/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/instrumentationprofile.json +42 -0
- pythonagent/cavisson/netdiagnostics/python/CavAgent/interceptor_points.txt +29 -0
- pythonagent/cavisson/netdiagnostics/python/config/ndsettings.conf +6 -0
- pythonagent/config.py +279 -0
- pythonagent/find.py +72 -0
- pythonagent/find_mod_cls_name.py +54 -0
- pythonagent/lang.py +131 -0
- pythonagent/lib.py +91 -0
- pythonagent/main/__init__.py +0 -0
- pythonagent/main/pytrace/__init__.py +79 -0
- pythonagent/main/pytrace/commands/__init__.py +0 -0
- pythonagent/main/pytrace/commands/auto_discovery.py +25 -0
- pythonagent/main/pytrace/commands/run.py +401 -0
- pythonagent/main/pytrace/pytrace.py +133 -0
- pythonagent/main/wsgi.py +6 -0
- pythonagent/main.py +27 -0
- pythonagent/run.py +46 -0
- pythonagent/sqins.py +8 -0
- pythonagent/test.py +73 -0
- pythonagent/utils.py +168 -0
- pythonagent/vendor/__init__.py +0 -0
- pythonagent/vendor/pympler/__init__.py +1 -0
- pythonagent/vendor/pympler/asizeof.py +2810 -0
- pythonagent/vendor/pympler/charts.py +62 -0
- pythonagent/vendor/pympler/classtracker.py +590 -0
- pythonagent/vendor/pympler/classtracker_stats.py +780 -0
- pythonagent/vendor/pympler/garbagegraph.py +80 -0
- pythonagent/vendor/pympler/mprofile.py +97 -0
- pythonagent/vendor/pympler/muppy.py +275 -0
- pythonagent/vendor/pympler/panels.py +115 -0
- pythonagent/vendor/pympler/process.py +238 -0
- pythonagent/vendor/pympler/py.typed +0 -0
- pythonagent/vendor/pympler/refbrowser.py +451 -0
- pythonagent/vendor/pympler/refgraph.py +350 -0
- pythonagent/vendor/pympler/summary.py +321 -0
- pythonagent/vendor/pympler/tracker.py +267 -0
- pythonagent/vendor/pympler/util/__init__.py +0 -0
- pythonagent/vendor/pympler/util/bottle.py +3809 -0
- pythonagent/vendor/pympler/util/compat.py +23 -0
- pythonagent/vendor/pympler/util/stringutils.py +77 -0
- pythonagent/vendor/pympler/web.py +346 -0
- pythonagent/vendor/werkzeug/__init__.py +20 -0
- pythonagent/vendor/werkzeug/_compat.py +228 -0
- pythonagent/vendor/werkzeug/_internal.py +473 -0
- pythonagent/vendor/werkzeug/_reloader.py +341 -0
- pythonagent/vendor/werkzeug/datastructures.py +3120 -0
- pythonagent/vendor/werkzeug/debug/__init__.py +498 -0
- pythonagent/vendor/werkzeug/debug/console.py +218 -0
- pythonagent/vendor/werkzeug/debug/repr.py +297 -0
- pythonagent/vendor/werkzeug/debug/tbtools.py +628 -0
- pythonagent/vendor/werkzeug/exceptions.py +829 -0
- pythonagent/vendor/werkzeug/filesystem.py +64 -0
- pythonagent/vendor/werkzeug/formparser.py +584 -0
- pythonagent/vendor/werkzeug/http.py +1307 -0
- pythonagent/vendor/werkzeug/local.py +420 -0
- pythonagent/vendor/werkzeug/middleware/__init__.py +25 -0
- pythonagent/vendor/werkzeug/middleware/dispatcher.py +66 -0
- pythonagent/vendor/werkzeug/middleware/http_proxy.py +219 -0
- pythonagent/vendor/werkzeug/middleware/lint.py +408 -0
- pythonagent/vendor/werkzeug/middleware/profiler.py +132 -0
- pythonagent/vendor/werkzeug/middleware/proxy_fix.py +169 -0
- pythonagent/vendor/werkzeug/middleware/shared_data.py +293 -0
- pythonagent/vendor/werkzeug/posixemulation.py +117 -0
- pythonagent/vendor/werkzeug/routing.py +2210 -0
- pythonagent/vendor/werkzeug/security.py +249 -0
- pythonagent/vendor/werkzeug/serving.py +1117 -0
- pythonagent/vendor/werkzeug/test.py +1123 -0
- pythonagent/vendor/werkzeug/testapp.py +241 -0
- pythonagent/vendor/werkzeug/urls.py +1138 -0
- pythonagent/vendor/werkzeug/useragents.py +202 -0
- pythonagent/vendor/werkzeug/utils.py +778 -0
- pythonagent/vendor/werkzeug/wrappers/__init__.py +36 -0
- pythonagent/vendor/werkzeug/wrappers/accept.py +50 -0
- pythonagent/vendor/werkzeug/wrappers/auth.py +33 -0
- pythonagent/vendor/werkzeug/wrappers/base_request.py +673 -0
- pythonagent/vendor/werkzeug/wrappers/base_response.py +700 -0
- pythonagent/vendor/werkzeug/wrappers/common_descriptors.py +341 -0
- pythonagent/vendor/werkzeug/wrappers/cors.py +100 -0
- pythonagent/vendor/werkzeug/wrappers/etag.py +304 -0
- pythonagent/vendor/werkzeug/wrappers/json.py +145 -0
- pythonagent/vendor/werkzeug/wrappers/request.py +49 -0
- pythonagent/vendor/werkzeug/wrappers/response.py +84 -0
- pythonagent/vendor/werkzeug/wrappers/user_agent.py +14 -0
- pythonagent/vendor/werkzeug/wsgi.py +1000 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic HTTP Proxy
|
|
3
|
+
================
|
|
4
|
+
|
|
5
|
+
.. autoclass:: ProxyMiddleware
|
|
6
|
+
|
|
7
|
+
:copyright: 2007 Pallets
|
|
8
|
+
:license: BSD-3-Clause
|
|
9
|
+
"""
|
|
10
|
+
import socket
|
|
11
|
+
|
|
12
|
+
from ..datastructures import EnvironHeaders
|
|
13
|
+
from ..http import is_hop_by_hop_header
|
|
14
|
+
from ..urls import url_parse
|
|
15
|
+
from ..urls import url_quote
|
|
16
|
+
from ..wsgi import get_input_stream
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from http import client
|
|
20
|
+
except ImportError:
|
|
21
|
+
import httplib as client
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProxyMiddleware(object):
|
|
25
|
+
"""Proxy requests under a path to an external server, routing other
|
|
26
|
+
requests to the app.
|
|
27
|
+
|
|
28
|
+
This middleware can only proxy HTTP requests, as that is the only
|
|
29
|
+
protocol handled by the WSGI server. Other protocols, such as
|
|
30
|
+
websocket requests, cannot be proxied at this layer. This should
|
|
31
|
+
only be used for development, in production a real proxying server
|
|
32
|
+
should be used.
|
|
33
|
+
|
|
34
|
+
The middleware takes a dict that maps a path prefix to a dict
|
|
35
|
+
describing the host to be proxied to::
|
|
36
|
+
|
|
37
|
+
app = ProxyMiddleware(app, {
|
|
38
|
+
"/static/": {
|
|
39
|
+
"target": "http://127.0.0.1:5001/",
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
Each host has the following options:
|
|
44
|
+
|
|
45
|
+
``target``:
|
|
46
|
+
The target URL to dispatch to. This is required.
|
|
47
|
+
``remove_prefix``:
|
|
48
|
+
Whether to remove the prefix from the URL before dispatching it
|
|
49
|
+
to the target. The default is ``False``.
|
|
50
|
+
``host``:
|
|
51
|
+
``"<auto>"`` (default):
|
|
52
|
+
The host header is automatically rewritten to the URL of the
|
|
53
|
+
target.
|
|
54
|
+
``None``:
|
|
55
|
+
The host header is unmodified from the client request.
|
|
56
|
+
Any other value:
|
|
57
|
+
The host header is overwritten with the value.
|
|
58
|
+
``headers``:
|
|
59
|
+
A dictionary of headers to be sent with the request to the
|
|
60
|
+
target. The default is ``{}``.
|
|
61
|
+
``ssl_context``:
|
|
62
|
+
A :class:`ssl.SSLContext` defining how to verify requests if the
|
|
63
|
+
target is HTTPS. The default is ``None``.
|
|
64
|
+
|
|
65
|
+
In the example above, everything under ``"/static/"`` is proxied to
|
|
66
|
+
the server on port 5001. The host header is rewritten to the target,
|
|
67
|
+
and the ``"/static/"`` prefix is removed from the URLs.
|
|
68
|
+
|
|
69
|
+
:param app: The WSGI application to wrap.
|
|
70
|
+
:param targets: Proxy target configurations. See description above.
|
|
71
|
+
:param chunk_size: Size of chunks to read from input stream and
|
|
72
|
+
write to target.
|
|
73
|
+
:param timeout: Seconds before an operation to a target fails.
|
|
74
|
+
|
|
75
|
+
.. versionadded:: 0.14
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, app, targets, chunk_size=2 << 13, timeout=10):
|
|
79
|
+
def _set_defaults(opts):
|
|
80
|
+
opts.setdefault("remove_prefix", False)
|
|
81
|
+
opts.setdefault("host", "<auto>")
|
|
82
|
+
opts.setdefault("headers", {})
|
|
83
|
+
opts.setdefault("ssl_context", None)
|
|
84
|
+
return opts
|
|
85
|
+
|
|
86
|
+
self.app = app
|
|
87
|
+
self.targets = dict(
|
|
88
|
+
("/%s/" % k.strip("/"), _set_defaults(v)) for k, v in targets.items()
|
|
89
|
+
)
|
|
90
|
+
self.chunk_size = chunk_size
|
|
91
|
+
self.timeout = timeout
|
|
92
|
+
|
|
93
|
+
def proxy_to(self, opts, path, prefix):
|
|
94
|
+
target = url_parse(opts["target"])
|
|
95
|
+
|
|
96
|
+
def application(environ, start_response):
|
|
97
|
+
headers = list(EnvironHeaders(environ).items())
|
|
98
|
+
headers[:] = [
|
|
99
|
+
(k, v)
|
|
100
|
+
for k, v in headers
|
|
101
|
+
if not is_hop_by_hop_header(k)
|
|
102
|
+
and k.lower() not in ("content-length", "host")
|
|
103
|
+
]
|
|
104
|
+
headers.append(("Connection", "close"))
|
|
105
|
+
|
|
106
|
+
if opts["host"] == "<auto>":
|
|
107
|
+
headers.append(("Host", target.ascii_host))
|
|
108
|
+
elif opts["host"] is None:
|
|
109
|
+
headers.append(("Host", environ["HTTP_HOST"]))
|
|
110
|
+
else:
|
|
111
|
+
headers.append(("Host", opts["host"]))
|
|
112
|
+
|
|
113
|
+
headers.extend(opts["headers"].items())
|
|
114
|
+
remote_path = path
|
|
115
|
+
|
|
116
|
+
if opts["remove_prefix"]:
|
|
117
|
+
remote_path = "%s/%s" % (
|
|
118
|
+
target.path.rstrip("/"),
|
|
119
|
+
remote_path[len(prefix) :].lstrip("/"),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
content_length = environ.get("CONTENT_LENGTH")
|
|
123
|
+
chunked = False
|
|
124
|
+
|
|
125
|
+
if content_length not in ("", None):
|
|
126
|
+
headers.append(("Content-Length", content_length))
|
|
127
|
+
elif content_length is not None:
|
|
128
|
+
headers.append(("Transfer-Encoding", "chunked"))
|
|
129
|
+
chunked = True
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
if target.scheme == "http":
|
|
133
|
+
con = client.HTTPConnection(
|
|
134
|
+
target.ascii_host, target.port or 80, timeout=self.timeout
|
|
135
|
+
)
|
|
136
|
+
elif target.scheme == "https":
|
|
137
|
+
con = client.HTTPSConnection(
|
|
138
|
+
target.ascii_host,
|
|
139
|
+
target.port or 443,
|
|
140
|
+
timeout=self.timeout,
|
|
141
|
+
context=opts["ssl_context"],
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
raise RuntimeError(
|
|
145
|
+
"Target scheme must be 'http' or 'https', got '{}'.".format(
|
|
146
|
+
target.scheme
|
|
147
|
+
)
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
con.connect()
|
|
151
|
+
remote_url = url_quote(remote_path)
|
|
152
|
+
querystring = environ["QUERY_STRING"]
|
|
153
|
+
|
|
154
|
+
if querystring:
|
|
155
|
+
remote_url = remote_url + "?" + querystring
|
|
156
|
+
|
|
157
|
+
con.putrequest(environ["REQUEST_METHOD"], remote_url, skip_host=True)
|
|
158
|
+
|
|
159
|
+
for k, v in headers:
|
|
160
|
+
if k.lower() == "connection":
|
|
161
|
+
v = "close"
|
|
162
|
+
|
|
163
|
+
con.putheader(k, v)
|
|
164
|
+
|
|
165
|
+
con.endheaders()
|
|
166
|
+
stream = get_input_stream(environ)
|
|
167
|
+
|
|
168
|
+
while 1:
|
|
169
|
+
data = stream.read(self.chunk_size)
|
|
170
|
+
|
|
171
|
+
if not data:
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
if chunked:
|
|
175
|
+
con.send(b"%x\r\n%s\r\n" % (len(data), data))
|
|
176
|
+
else:
|
|
177
|
+
con.send(data)
|
|
178
|
+
|
|
179
|
+
resp = con.getresponse()
|
|
180
|
+
except socket.error:
|
|
181
|
+
from ..exceptions import BadGateway
|
|
182
|
+
|
|
183
|
+
return BadGateway()(environ, start_response)
|
|
184
|
+
|
|
185
|
+
start_response(
|
|
186
|
+
"%d %s" % (resp.status, resp.reason),
|
|
187
|
+
[
|
|
188
|
+
(k.title(), v)
|
|
189
|
+
for k, v in resp.getheaders()
|
|
190
|
+
if not is_hop_by_hop_header(k)
|
|
191
|
+
],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
def read():
|
|
195
|
+
while 1:
|
|
196
|
+
try:
|
|
197
|
+
data = resp.read(self.chunk_size)
|
|
198
|
+
except socket.error:
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
if not data:
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
yield data
|
|
205
|
+
|
|
206
|
+
return read()
|
|
207
|
+
|
|
208
|
+
return application
|
|
209
|
+
|
|
210
|
+
def __call__(self, environ, start_response):
|
|
211
|
+
path = environ["PATH_INFO"]
|
|
212
|
+
app = self.app
|
|
213
|
+
|
|
214
|
+
for prefix, opts in self.targets.items():
|
|
215
|
+
if path.startswith(prefix):
|
|
216
|
+
app = self.proxy_to(opts, path, prefix)
|
|
217
|
+
break
|
|
218
|
+
|
|
219
|
+
return app(environ, start_response)
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
"""
|
|
2
|
+
WSGI Protocol Linter
|
|
3
|
+
====================
|
|
4
|
+
|
|
5
|
+
This module provides a middleware that performs sanity checks on the
|
|
6
|
+
behavior of the WSGI server and application. It checks that the
|
|
7
|
+
:pep:`3333` WSGI spec is properly implemented. It also warns on some
|
|
8
|
+
common HTTP errors such as non-empty responses for 304 status codes.
|
|
9
|
+
|
|
10
|
+
.. autoclass:: LintMiddleware
|
|
11
|
+
|
|
12
|
+
:copyright: 2007 Pallets
|
|
13
|
+
:license: BSD-3-Clause
|
|
14
|
+
"""
|
|
15
|
+
from warnings import warn
|
|
16
|
+
|
|
17
|
+
from .._compat import implements_iterator
|
|
18
|
+
from .._compat import PY2
|
|
19
|
+
from .._compat import string_types
|
|
20
|
+
from ..datastructures import Headers
|
|
21
|
+
from ..http import is_entity_header
|
|
22
|
+
from ..wsgi import FileWrapper
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from urllib.parse import urlparse
|
|
26
|
+
except ImportError:
|
|
27
|
+
from urlparse import urlparse
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class WSGIWarning(Warning):
|
|
31
|
+
"""Warning class for WSGI warnings."""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class HTTPWarning(Warning):
|
|
35
|
+
"""Warning class for HTTP warnings."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def check_string(context, obj, stacklevel=3):
|
|
39
|
+
if type(obj) is not str:
|
|
40
|
+
warn(
|
|
41
|
+
"'%s' requires strings, got '%s'" % (context, type(obj).__name__),
|
|
42
|
+
WSGIWarning,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class InputStream(object):
|
|
47
|
+
def __init__(self, stream):
|
|
48
|
+
self._stream = stream
|
|
49
|
+
|
|
50
|
+
def read(self, *args):
|
|
51
|
+
if len(args) == 0:
|
|
52
|
+
warn(
|
|
53
|
+
"WSGI does not guarantee an EOF marker on the input stream, thus making"
|
|
54
|
+
" calls to 'wsgi.input.read()' unsafe. Conforming servers may never"
|
|
55
|
+
" return from this call.",
|
|
56
|
+
WSGIWarning,
|
|
57
|
+
stacklevel=2,
|
|
58
|
+
)
|
|
59
|
+
elif len(args) != 1:
|
|
60
|
+
warn(
|
|
61
|
+
"Too many parameters passed to 'wsgi.input.read()'.",
|
|
62
|
+
WSGIWarning,
|
|
63
|
+
stacklevel=2,
|
|
64
|
+
)
|
|
65
|
+
return self._stream.read(*args)
|
|
66
|
+
|
|
67
|
+
def readline(self, *args):
|
|
68
|
+
if len(args) == 0:
|
|
69
|
+
warn(
|
|
70
|
+
"Calls to 'wsgi.input.readline()' without arguments are unsafe. Use"
|
|
71
|
+
" 'wsgi.input.read()' instead.",
|
|
72
|
+
WSGIWarning,
|
|
73
|
+
stacklevel=2,
|
|
74
|
+
)
|
|
75
|
+
elif len(args) == 1:
|
|
76
|
+
warn(
|
|
77
|
+
"'wsgi.input.readline()' was called with a size hint. WSGI does not"
|
|
78
|
+
" support this, although it's available on all major servers.",
|
|
79
|
+
WSGIWarning,
|
|
80
|
+
stacklevel=2,
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
raise TypeError("Too many arguments passed to 'wsgi.input.readline()'.")
|
|
84
|
+
return self._stream.readline(*args)
|
|
85
|
+
|
|
86
|
+
def __iter__(self):
|
|
87
|
+
try:
|
|
88
|
+
return iter(self._stream)
|
|
89
|
+
except TypeError:
|
|
90
|
+
warn("'wsgi.input' is not iterable.", WSGIWarning, stacklevel=2)
|
|
91
|
+
return iter(())
|
|
92
|
+
|
|
93
|
+
def close(self):
|
|
94
|
+
warn("The application closed the input stream!", WSGIWarning, stacklevel=2)
|
|
95
|
+
self._stream.close()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ErrorStream(object):
|
|
99
|
+
def __init__(self, stream):
|
|
100
|
+
self._stream = stream
|
|
101
|
+
|
|
102
|
+
def write(self, s):
|
|
103
|
+
check_string("wsgi.error.write()", s)
|
|
104
|
+
self._stream.write(s)
|
|
105
|
+
|
|
106
|
+
def flush(self):
|
|
107
|
+
self._stream.flush()
|
|
108
|
+
|
|
109
|
+
def writelines(self, seq):
|
|
110
|
+
for line in seq:
|
|
111
|
+
self.write(line)
|
|
112
|
+
|
|
113
|
+
def close(self):
|
|
114
|
+
warn("The application closed the error stream!", WSGIWarning, stacklevel=2)
|
|
115
|
+
self._stream.close()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GuardedWrite(object):
|
|
119
|
+
def __init__(self, write, chunks):
|
|
120
|
+
self._write = write
|
|
121
|
+
self._chunks = chunks
|
|
122
|
+
|
|
123
|
+
def __call__(self, s):
|
|
124
|
+
check_string("write()", s)
|
|
125
|
+
self._write.write(s)
|
|
126
|
+
self._chunks.append(len(s))
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@implements_iterator
|
|
130
|
+
class GuardedIterator(object):
|
|
131
|
+
def __init__(self, iterator, headers_set, chunks):
|
|
132
|
+
self._iterator = iterator
|
|
133
|
+
if PY2:
|
|
134
|
+
self._next = iter(iterator).next
|
|
135
|
+
else:
|
|
136
|
+
self._next = iter(iterator).__next__
|
|
137
|
+
self.closed = False
|
|
138
|
+
self.headers_set = headers_set
|
|
139
|
+
self.chunks = chunks
|
|
140
|
+
|
|
141
|
+
def __iter__(self):
|
|
142
|
+
return self
|
|
143
|
+
|
|
144
|
+
def __next__(self):
|
|
145
|
+
if self.closed:
|
|
146
|
+
warn("Iterated over closed 'app_iter'.", WSGIWarning, stacklevel=2)
|
|
147
|
+
|
|
148
|
+
rv = self._next()
|
|
149
|
+
|
|
150
|
+
if not self.headers_set:
|
|
151
|
+
warn(
|
|
152
|
+
"The application returned before it started the response.",
|
|
153
|
+
WSGIWarning,
|
|
154
|
+
stacklevel=2,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
check_string("application iterator items", rv)
|
|
158
|
+
self.chunks.append(len(rv))
|
|
159
|
+
return rv
|
|
160
|
+
|
|
161
|
+
def close(self):
|
|
162
|
+
self.closed = True
|
|
163
|
+
|
|
164
|
+
if hasattr(self._iterator, "close"):
|
|
165
|
+
self._iterator.close()
|
|
166
|
+
|
|
167
|
+
if self.headers_set:
|
|
168
|
+
status_code, headers = self.headers_set
|
|
169
|
+
bytes_sent = sum(self.chunks)
|
|
170
|
+
content_length = headers.get("content-length", type=int)
|
|
171
|
+
|
|
172
|
+
if status_code == 304:
|
|
173
|
+
for key, _value in headers:
|
|
174
|
+
key = key.lower()
|
|
175
|
+
if key not in ("expires", "content-location") and is_entity_header(
|
|
176
|
+
key
|
|
177
|
+
):
|
|
178
|
+
warn(
|
|
179
|
+
"Entity header %r found in 304 response." % key, HTTPWarning
|
|
180
|
+
)
|
|
181
|
+
if bytes_sent:
|
|
182
|
+
warn("304 responses must not have a body.", HTTPWarning)
|
|
183
|
+
elif 100 <= status_code < 200 or status_code == 204:
|
|
184
|
+
if content_length != 0:
|
|
185
|
+
warn(
|
|
186
|
+
"%r responses must have an empty content length." % status_code,
|
|
187
|
+
HTTPWarning,
|
|
188
|
+
)
|
|
189
|
+
if bytes_sent:
|
|
190
|
+
warn(
|
|
191
|
+
"%r responses must not have a body." % status_code, HTTPWarning
|
|
192
|
+
)
|
|
193
|
+
elif content_length is not None and content_length != bytes_sent:
|
|
194
|
+
warn(
|
|
195
|
+
"Content-Length and the number of bytes sent to the client do not"
|
|
196
|
+
" match.",
|
|
197
|
+
WSGIWarning,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
def __del__(self):
|
|
201
|
+
if not self.closed:
|
|
202
|
+
try:
|
|
203
|
+
warn(
|
|
204
|
+
"Iterator was garbage collected before it was closed.", WSGIWarning
|
|
205
|
+
)
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class LintMiddleware(object):
|
|
211
|
+
"""Warns about common errors in the WSGI and HTTP behavior of the
|
|
212
|
+
server and wrapped application. Some of the issues it check are:
|
|
213
|
+
|
|
214
|
+
- invalid status codes
|
|
215
|
+
- non-bytestrings sent to the WSGI server
|
|
216
|
+
- strings returned from the WSGI application
|
|
217
|
+
- non-empty conditional responses
|
|
218
|
+
- unquoted etags
|
|
219
|
+
- relative URLs in the Location header
|
|
220
|
+
- unsafe calls to wsgi.input
|
|
221
|
+
- unclosed iterators
|
|
222
|
+
|
|
223
|
+
Error information is emitted using the :mod:`warnings` module.
|
|
224
|
+
|
|
225
|
+
:param app: The WSGI application to wrap.
|
|
226
|
+
|
|
227
|
+
.. code-block:: python
|
|
228
|
+
|
|
229
|
+
from pythonagent.vendor.werkzeug.middleware.lint import LintMiddleware
|
|
230
|
+
app = LintMiddleware(app)
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
def __init__(self, app):
|
|
234
|
+
self.app = app
|
|
235
|
+
|
|
236
|
+
def check_environ(self, environ):
|
|
237
|
+
if type(environ) is not dict:
|
|
238
|
+
warn(
|
|
239
|
+
"WSGI environment is not a standard Python dict.",
|
|
240
|
+
WSGIWarning,
|
|
241
|
+
stacklevel=4,
|
|
242
|
+
)
|
|
243
|
+
for key in (
|
|
244
|
+
"REQUEST_METHOD",
|
|
245
|
+
"SERVER_NAME",
|
|
246
|
+
"SERVER_PORT",
|
|
247
|
+
"wsgi.version",
|
|
248
|
+
"wsgi.input",
|
|
249
|
+
"wsgi.errors",
|
|
250
|
+
"wsgi.multithread",
|
|
251
|
+
"wsgi.multiprocess",
|
|
252
|
+
"wsgi.run_once",
|
|
253
|
+
):
|
|
254
|
+
if key not in environ:
|
|
255
|
+
warn(
|
|
256
|
+
"Required environment key %r not found" % key,
|
|
257
|
+
WSGIWarning,
|
|
258
|
+
stacklevel=3,
|
|
259
|
+
)
|
|
260
|
+
if environ["wsgi.version"] != (1, 0):
|
|
261
|
+
warn("Environ is not a WSGI 1.0 environ.", WSGIWarning, stacklevel=3)
|
|
262
|
+
|
|
263
|
+
script_name = environ.get("SCRIPT_NAME", "")
|
|
264
|
+
path_info = environ.get("PATH_INFO", "")
|
|
265
|
+
|
|
266
|
+
if script_name and script_name[0] != "/":
|
|
267
|
+
warn(
|
|
268
|
+
"'SCRIPT_NAME' does not start with a slash: %r" % script_name,
|
|
269
|
+
WSGIWarning,
|
|
270
|
+
stacklevel=3,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if path_info and path_info[0] != "/":
|
|
274
|
+
warn(
|
|
275
|
+
"'PATH_INFO' does not start with a slash: %r" % path_info,
|
|
276
|
+
WSGIWarning,
|
|
277
|
+
stacklevel=3,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def check_start_response(self, status, headers, exc_info):
|
|
281
|
+
check_string("status", status)
|
|
282
|
+
status_code = status.split(None, 1)[0]
|
|
283
|
+
|
|
284
|
+
if len(status_code) != 3 or not status_code.isdigit():
|
|
285
|
+
warn(WSGIWarning("Status code must be three digits"), stacklevel=3)
|
|
286
|
+
|
|
287
|
+
if len(status) < 4 or status[3] != " ":
|
|
288
|
+
warn(
|
|
289
|
+
WSGIWarning(
|
|
290
|
+
"Invalid value for status %r. Valid "
|
|
291
|
+
"status strings are three digits, a space "
|
|
292
|
+
"and a status explanation"
|
|
293
|
+
),
|
|
294
|
+
stacklevel=3,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
status_code = int(status_code)
|
|
298
|
+
|
|
299
|
+
if status_code < 100:
|
|
300
|
+
warn(WSGIWarning("status code < 100 detected"), stacklevel=3)
|
|
301
|
+
|
|
302
|
+
if type(headers) is not list:
|
|
303
|
+
warn(WSGIWarning("header list is not a list"), stacklevel=3)
|
|
304
|
+
|
|
305
|
+
for item in headers:
|
|
306
|
+
if type(item) is not tuple or len(item) != 2:
|
|
307
|
+
warn(WSGIWarning("Headers must tuple 2-item tuples"), stacklevel=3)
|
|
308
|
+
name, value = item
|
|
309
|
+
if type(name) is not str or type(value) is not str:
|
|
310
|
+
warn(WSGIWarning("header items must be strings"), stacklevel=3)
|
|
311
|
+
if name.lower() == "status":
|
|
312
|
+
warn(
|
|
313
|
+
WSGIWarning(
|
|
314
|
+
"The status header is not supported due to "
|
|
315
|
+
"conflicts with the CGI spec."
|
|
316
|
+
),
|
|
317
|
+
stacklevel=3,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
if exc_info is not None and not isinstance(exc_info, tuple):
|
|
321
|
+
warn(WSGIWarning("invalid value for exc_info"), stacklevel=3)
|
|
322
|
+
|
|
323
|
+
headers = Headers(headers)
|
|
324
|
+
self.check_headers(headers)
|
|
325
|
+
|
|
326
|
+
return status_code, headers
|
|
327
|
+
|
|
328
|
+
def check_headers(self, headers):
|
|
329
|
+
etag = headers.get("etag")
|
|
330
|
+
|
|
331
|
+
if etag is not None:
|
|
332
|
+
if etag.startswith(("W/", "w/")):
|
|
333
|
+
if etag.startswith("w/"):
|
|
334
|
+
warn(
|
|
335
|
+
HTTPWarning("weak etag indicator should be upcase."),
|
|
336
|
+
stacklevel=4,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
etag = etag[2:]
|
|
340
|
+
|
|
341
|
+
if not (etag[:1] == etag[-1:] == '"'):
|
|
342
|
+
warn(HTTPWarning("unquoted etag emitted."), stacklevel=4)
|
|
343
|
+
|
|
344
|
+
location = headers.get("location")
|
|
345
|
+
|
|
346
|
+
if location is not None:
|
|
347
|
+
if not urlparse(location).netloc:
|
|
348
|
+
warn(
|
|
349
|
+
HTTPWarning("absolute URLs required for location header"),
|
|
350
|
+
stacklevel=4,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def check_iterator(self, app_iter):
|
|
354
|
+
if isinstance(app_iter, string_types):
|
|
355
|
+
warn(
|
|
356
|
+
"The application returned astring. The response will send one character"
|
|
357
|
+
" at a time to the client, which will kill performance. Return a list"
|
|
358
|
+
" or iterable instead.",
|
|
359
|
+
WSGIWarning,
|
|
360
|
+
stacklevel=3,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def __call__(self, *args, **kwargs):
|
|
364
|
+
if len(args) != 2:
|
|
365
|
+
warn("A WSGI app takes two arguments.", WSGIWarning, stacklevel=2)
|
|
366
|
+
|
|
367
|
+
if kwargs:
|
|
368
|
+
warn(
|
|
369
|
+
"A WSGI app does not take keyword arguments.", WSGIWarning, stacklevel=2
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
environ, start_response = args
|
|
373
|
+
|
|
374
|
+
self.check_environ(environ)
|
|
375
|
+
environ["wsgi.input"] = InputStream(environ["wsgi.input"])
|
|
376
|
+
environ["wsgi.errors"] = ErrorStream(environ["wsgi.errors"])
|
|
377
|
+
|
|
378
|
+
# Hook our own file wrapper in so that applications will always
|
|
379
|
+
# iterate to the end and we can check the content length.
|
|
380
|
+
environ["wsgi.file_wrapper"] = FileWrapper
|
|
381
|
+
|
|
382
|
+
headers_set = []
|
|
383
|
+
chunks = []
|
|
384
|
+
|
|
385
|
+
def checking_start_response(*args, **kwargs):
|
|
386
|
+
if len(args) not in (2, 3):
|
|
387
|
+
warn(
|
|
388
|
+
"Invalid number of arguments: %s, expected 2 or 3." % len(args),
|
|
389
|
+
WSGIWarning,
|
|
390
|
+
stacklevel=2,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
if kwargs:
|
|
394
|
+
warn("'start_response' does not take keyword arguments.", WSGIWarning)
|
|
395
|
+
|
|
396
|
+
status, headers = args[:2]
|
|
397
|
+
|
|
398
|
+
if len(args) == 3:
|
|
399
|
+
exc_info = args[2]
|
|
400
|
+
else:
|
|
401
|
+
exc_info = None
|
|
402
|
+
|
|
403
|
+
headers_set[:] = self.check_start_response(status, headers, exc_info)
|
|
404
|
+
return GuardedWrite(start_response(status, headers, exc_info), chunks)
|
|
405
|
+
|
|
406
|
+
app_iter = self.app(environ, checking_start_response)
|
|
407
|
+
self.check_iterator(app_iter)
|
|
408
|
+
return GuardedIterator(app_iter, headers_set, chunks)
|