Quellcode durchsuchen

Rewrite webdav http request/response dumping compatible with windows

Bastien Sevajol (Algoo) vor 7 Jahren
Ursprung
Commit
76f334e87f
2 geänderte Dateien mit 230 neuen und 91 gelöschten Zeilen
  1. 9 91
      tracim/tracim/lib/daemons.py
  2. 221 0
      tracim/tracim/lib/webdav/utils.py

+ 9 - 91
tracim/tracim/lib/daemons.py Datei anzeigen

@@ -259,99 +259,12 @@ from inspect import isfunction
259 259
 import traceback
260 260
 
261 261
 from wsgidav.server.cherrypy import wsgiserver
262
-from wsgidav.server.cherrypy.wsgiserver.wsgiserver3 import \
263
-    HTTPConnection as BaseHTTPConnection
264
-from wsgidav.server.cherrypy.wsgiserver.wsgiserver3 import \
265
-    HTTPRequest as BaseHTTPRequest
262
+from wsgidav.server.cherrypy.wsgiserver.wsgiserver3 import CherryPyWSGIServer
266 263
 
267 264
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
268 265
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
269 266
 
270 267
 
271
-class HTTPRequest(BaseHTTPRequest):
272
-    """
273
-    Exeprimental override of HTTPRequest designed to permit
274
-    dump of HTTP requests
275
-    """
276
-    def parse_request(self, can_dump: bool=True):
277
-        super().parse_request()
278
-        dump = self.server.wsgi_app.config.get('dump_requests', False)
279
-        if self.ready and dump and can_dump:
280
-            dump_to_path = self.server.wsgi_app.config.get(
281
-                'dump_requests_path',
282
-                '/tmp/wsgidav_dumps',
283
-            )
284
-            self.dump(dump_to_path)
285
-
286
-    def dump(self, dump_to_path: str):
287
-        if self.ready:
288
-            os.makedirs(dump_to_path, exist_ok=True)
289
-            dump_file = '{0}/{1}_{2}.yml'.format(
290
-                dump_to_path,
291
-                '{0}_{1}'.format(
292
-                    datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S'),
293
-                    int(round(time.time() * 1000)),
294
-                ),
295
-                self.method.decode('utf-8'),
296
-            )
297
-            with open(dump_file, 'w+') as f:
298
-                dump_content = dict()
299
-                dump_content['path'] = self.path.decode('ISO-8859-1')
300
-                str_headers = dict(
301
-                    (k.decode('utf8'), v.decode('ISO-8859-1')) for k, v in
302
-                    self.inheaders.items()
303
-                )
304
-                dump_content['headers'] = str_headers
305
-                dump_content['content'] = self.conn.read_content\
306
-                    .decode('ISO-8859-1')
307
-
308
-                f.write(yaml.dump(dump_content, default_flow_style=False))
309
-
310
-
311
-class HTTPConnection(BaseHTTPConnection):
312
-    """
313
-    Exeprimental override of HTTPConnection designed to permit
314
-    dump of HTTP requests
315
-    """
316
-    RequestHandlerClass = HTTPRequest
317
-
318
-    def __init__(self, server, sock, *args, **kwargs):
319
-        super().__init__(server, sock, *args, **kwargs)
320
-
321
-        if self.server.wsgi_app.config.get('dump_requests', False):
322
-            # We use HTTPRequest to parse headers, path, etc ...
323
-            req = self.RequestHandlerClass(self.server, self)
324
-            req.parse_request(can_dump=False)
325
-
326
-            # And we read request content
327
-            content_length = int(req.inheaders.get(b'Content-Length', b'0'))
328
-            content = req.rfile.read(content_length)
329
-
330
-            # We are now able to rebuild HTTP request
331
-            full_content = \
332
-                req.method + \
333
-                b' ' + \
334
-                req.uri + \
335
-                b' ' + \
336
-                req.request_protocol +\
337
-                b'\r\n'
338
-            for header_name, header_value in req.inheaders.items():
339
-                full_content += header_name + b': ' + header_value + b'\r\n'
340
-
341
-            full_content += b'\r\n' + content
342
-
343
-            # To give it again at HTTPConnection
344
-            bf = io.BufferedReader(io.BytesIO(full_content), self.rbufsize)
345
-
346
-            self.rfile = bf
347
-            # We will be able to dump request content with self.read_content
348
-            self.read_content = content
349
-
350
-
351
-class CherryPyWSGIServer(wsgiserver.CherryPyWSGIServer):
352
-    ConnectionClass = HTTPConnection
353
-
354
-
355 268
 class WsgiDavDaemon(Daemon):
356 269
 
357 270
     def __init__(self, *args, **kwargs):
@@ -377,11 +290,16 @@ class WsgiDavDaemon(Daemon):
377 290
             print(
378 291
                 "WARNING: Could not import lxml: using xml instead (slower). Consider installing lxml from http://codespeak.net/lxml/.")
379 292
         from wsgidav.dir_browser import WsgiDavDirBrowser
380
-        from wsgidav.debug_filter import WsgiDavDebugFilter
381 293
         from tracim.lib.webdav.tracim_http_authenticator import TracimHTTPAuthenticator
382 294
         from wsgidav.error_printer import ErrorPrinter
383
-
384
-        config['middleware_stack'] = [ WsgiDavDirBrowser, TracimHTTPAuthenticator, ErrorPrinter, WsgiDavDebugFilter ]
295
+        from tracim.lib.webdav.utils import TracimWsgiDavDebugFilter
296
+
297
+        config['middleware_stack'] = [
298
+            WsgiDavDirBrowser,
299
+            TracimHTTPAuthenticator,
300
+            ErrorPrinter,
301
+            TracimWsgiDavDebugFilter,
302
+        ]
385 303
 
386 304
         config['provider_mapping'] = {
387 305
             config['root_path']: Provider(

+ 221 - 0
tracim/tracim/lib/webdav/utils.py Datei anzeigen

@@ -1,4 +1,16 @@
1 1
 # -*- coding: utf-8 -*-
2
+from xml.etree import ElementTree
3
+
4
+import os
5
+import sys
6
+import threading
7
+import time
8
+from datetime import datetime
9
+
10
+import yaml
11
+from wsgidav import util
12
+from wsgidav import compat
13
+from wsgidav.middleware import BaseMiddleware
2 14
 
3 15
 
4 16
 def transform_to_display(string: str) -> str:
@@ -47,3 +59,212 @@ def transform_to_bdd(string: str) -> str:
47 59
         string = string.replace(key, value)
48 60
 
49 61
     return string
62
+
63
+
64
+class TracimWsgiDavDebugFilter(BaseMiddleware):
65
+    """
66
+    COPY PASTE OF wsgidav.debug_filter.WsgiDavDebugFilter
67
+    WITH ADD OF DUMP RESPONSE & REQUEST
68
+    """
69
+    def __init__(self, application, config):
70
+        self._application = application
71
+        self._config = config
72
+        #        self.out = sys.stderr
73
+        self.out = sys.stdout
74
+        self.passedLitmus = {}
75
+        # These methods boost verbose=2 to verbose=3
76
+        self.debug_methods = config.get("debug_methods", [])
77
+        # Litmus tests containing these string boost verbose=2 to verbose=3
78
+        self.debug_litmus = config.get("debug_litmus", [])
79
+        # Exit server, as soon as this litmus test has finished
80
+        self.break_after_litmus = [
81
+            #                                   "locks: 15",
82
+        ]
83
+
84
+        if self._config.get('dump_requests'):
85
+            # Monkey patching
86
+            old_parseXmlBody = util.parseXmlBody
87
+            def new_parseXmlBody(environ, allowEmpty=False):
88
+                xml = old_parseXmlBody(environ, allowEmpty)
89
+                self._dump_request(environ, xml)
90
+                return xml
91
+            util.parseXmlBody = new_parseXmlBody
92
+
93
+    def __call__(self, environ, start_response):
94
+        """"""
95
+        #        srvcfg = environ["wsgidav.config"]
96
+        verbose = self._config.get("verbose", 2)
97
+
98
+        method = environ["REQUEST_METHOD"]
99
+
100
+        debugBreak = False
101
+        dumpRequest = False
102
+        dumpResponse = False
103
+
104
+        if verbose >= 3 or self._config.get("dump_requests"):
105
+            dumpRequest = dumpResponse = True
106
+
107
+        # Process URL commands
108
+        if "dump_storage" in environ.get("QUERY_STRING"):
109
+            dav = environ.get("wsgidav.provider")
110
+            if dav.lockManager:
111
+                dav.lockManager._dump()
112
+            if dav.propManager:
113
+                dav.propManager._dump()
114
+
115
+        # Turn on max. debugging for selected litmus tests
116
+        litmusTag = environ.get("HTTP_X_LITMUS",
117
+                                environ.get("HTTP_X_LITMUS_SECOND"))
118
+        if litmusTag and verbose >= 2:
119
+            print("----\nRunning litmus test '%s'..." % litmusTag,
120
+                  file=self.out)
121
+            for litmusSubstring in self.debug_litmus:
122
+                if litmusSubstring in litmusTag:
123
+                    verbose = 3
124
+                    debugBreak = True
125
+                    dumpRequest = True
126
+                    dumpResponse = True
127
+                    break
128
+            for litmusSubstring in self.break_after_litmus:
129
+                if litmusSubstring in self.passedLitmus and litmusSubstring not in litmusTag:
130
+                    print(" *** break after litmus %s" % litmusTag,
131
+                          file=self.out)
132
+                    sys.exit(-1)
133
+                if litmusSubstring in litmusTag:
134
+                    self.passedLitmus[litmusSubstring] = True
135
+
136
+        # Turn on max. debugging for selected request methods
137
+        if verbose >= 2 and method in self.debug_methods:
138
+            verbose = 3
139
+            debugBreak = True
140
+            dumpRequest = True
141
+            dumpResponse = True
142
+
143
+        # Set debug options to environment
144
+        environ["wsgidav.verbose"] = verbose
145
+        #        environ["wsgidav.debug_methods"] = self.debug_methods
146
+        environ["wsgidav.debug_break"] = debugBreak
147
+        environ["wsgidav.dump_request_body"] = dumpRequest
148
+        environ["wsgidav.dump_response_body"] = dumpResponse
149
+
150
+        # Dump request headers
151
+        if dumpRequest:
152
+            print("<%s> --- %s Request ---" % (
153
+            threading.currentThread().ident, method), file=self.out)
154
+            for k, v in environ.items():
155
+                if k == k.upper():
156
+                    print("%20s: '%s'" % (k, v), file=self.out)
157
+            print("\n", file=self.out)
158
+
159
+        # Intercept start_response
160
+        #
161
+        sub_app_start_response = util.SubAppStartResponse()
162
+
163
+        nbytes = 0
164
+        first_yield = True
165
+        app_iter = self._application(environ, sub_app_start_response)
166
+
167
+        for v in app_iter:
168
+            # Start response (the first time)
169
+            if first_yield:
170
+                # Success!
171
+                start_response(sub_app_start_response.status,
172
+                               sub_app_start_response.response_headers,
173
+                               sub_app_start_response.exc_info)
174
+
175
+            # Dump response headers
176
+            if first_yield and dumpResponse:
177
+                print("<%s> --- %s Response(%s): ---" % (
178
+                threading.currentThread().ident,
179
+                method,
180
+                sub_app_start_response.status),
181
+                      file=self.out)
182
+                headersdict = dict(sub_app_start_response.response_headers)
183
+                for envitem in headersdict.keys():
184
+                    print("%s: %s" % (envitem, repr(headersdict[envitem])),
185
+                          file=self.out)
186
+                print("", file=self.out)
187
+
188
+            # Check, if response is a binary string, otherwise we probably have
189
+            # calculated a wrong content-length
190
+            assert compat.is_bytes(v), v
191
+
192
+            # Dump response body
193
+            drb = environ.get("wsgidav.dump_response_body")
194
+            if compat.is_basestring(drb):
195
+                # Middleware provided a formatted body representation
196
+                print(drb, file=self.out)
197
+            elif drb is True:
198
+                # Else dump what we get, (except for long GET responses)
199
+                if method == "GET":
200
+                    if first_yield:
201
+                        print(v[:50], "...", file=self.out)
202
+                elif len(v) > 0:
203
+                    print(v, file=self.out)
204
+
205
+            if dumpResponse:
206
+                self._dump_response(sub_app_start_response, drb)
207
+
208
+            drb = environ["wsgidav.dump_response_body"] = None
209
+
210
+            nbytes += len(v)
211
+            first_yield = False
212
+            yield v
213
+        if hasattr(app_iter, "close"):
214
+            app_iter.close()
215
+
216
+        # Start response (if it hasn't been done yet)
217
+        if first_yield:
218
+            # Success!
219
+            start_response(sub_app_start_response.status,
220
+                           sub_app_start_response.response_headers,
221
+                           sub_app_start_response.exc_info)
222
+
223
+        if dumpResponse:
224
+            print("\n<%s> --- End of %s Response (%i bytes) ---" % (
225
+            threading.currentThread().ident, method, nbytes), file=self.out)
226
+        return
227
+
228
+    def _dump_response(self, sub_app_start_response, drb):
229
+        dump_to_path = self._config.get(
230
+            'dump_requests_path',
231
+            '/tmp/wsgidav_dumps',
232
+        )
233
+        os.makedirs(dump_to_path, exist_ok=True)
234
+        dump_file = '{0}/{1}_RESPONSE_{2}.yml'.format(
235
+            dump_to_path,
236
+            '{0}_{1}'.format(
237
+                datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S'),
238
+                int(round(time.time() * 1000)),
239
+            ),
240
+            sub_app_start_response.status[0:3],
241
+        )
242
+        with open(dump_file, 'w+') as f:
243
+            dump_content = dict()
244
+            if isinstance(drb, str):
245
+                dump_content['content'] = drb.replace('PROPFIND XML response body:\n', '')
246
+
247
+            f.write(yaml.dump(dump_content, default_flow_style=False))
248
+
249
+    def _dump_request(self, environ, xml):
250
+        dump_to_path = self._config.get(
251
+            'dump_requests_path',
252
+            '/tmp/wsgidav_dumps',
253
+        )
254
+        os.makedirs(dump_to_path, exist_ok=True)
255
+        dump_file = '{0}/{1}_REQUEST_{2}.yml'.format(
256
+            dump_to_path,
257
+            '{0}_{1}'.format(
258
+                datetime.utcnow().strftime('%Y-%m-%d_%H-%M-%S'),
259
+                int(round(time.time() * 1000)),
260
+            ),
261
+            environ['REQUEST_METHOD'],
262
+        )
263
+        with open(dump_file, 'w+') as f:
264
+            dump_content = dict()
265
+            dump_content['path'] = environ['PATH_INFO']
266
+            dump_content['Authorization'] = environ['HTTP_AUTHORIZATION']
267
+            if xml:
268
+                dump_content['content'] = ElementTree.tostring(xml, 'utf-8')
269
+
270
+            f.write(yaml.dump(dump_content, default_flow_style=False))