Browse Source

WebDav HTTP dump of requests

Bastien Sevajol (Algoo) 7 years ago
parent
commit
5d374ca742
3 changed files with 117 additions and 31 deletions
  1. 1 1
      install/requirements.txt
  2. 113 29
      tracim/tracim/lib/daemons.py
  3. 3 1
      tracim/tracim/lib/webdav/sql_resources.py

+ 1 - 1
install/requirements.txt View File

@@ -62,4 +62,4 @@ who-ldap==3.1.0
62 62
 -e git+https://github.com/algoo/wsgidav.git@py3#egg=wsgidav
63 63
 zope.interface==4.1.3
64 64
 zope.sqlalchemy==0.7.6
65
-
65
+PyYAML

+ 113 - 29
tracim/tracim/lib/daemons.py View File

@@ -1,10 +1,14 @@
1 1
 import threading
2 2
 from configparser import DuplicateSectionError
3
+from datetime import datetime
3 4
 from wsgiref.simple_server import make_server
4 5
 import signal
5 6
 
6 7
 import collections
7
-import transaction
8
+import time
9
+
10
+import io
11
+import yaml
8 12
 
9 13
 from radicale import Application as RadicaleApplication
10 14
 from radicale import HTTPServer as BaseRadicaleHTTPServer
@@ -254,10 +258,100 @@ from tracim.lib.webdav.sql_domain_controller import TracimDomainController
254 258
 from inspect import isfunction
255 259
 import traceback
256 260
 
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
266
+
257 267
 DEFAULT_CONFIG_FILE = "wsgidav.conf"
258 268
 PYTHON_VERSION = "%s.%s.%s" % (sys.version_info[0], sys.version_info[1], sys.version_info[2])
259 269
 
260 270
 
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-%I-%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
+
261 355
 class WsgiDavDaemon(Daemon):
262 356
 
263 357
     def __init__(self, *args, **kwargs):
@@ -334,39 +428,29 @@ class WsgiDavDaemon(Daemon):
334 428
         app = WsgiDAVApp(self.config)
335 429
 
336 430
         # Try running WsgiDAV inside the following external servers:
337
-        self._runCherryPy(app, self.config, "cherrypy-bundled")
338
-
339
-    def _runCherryPy(self, app, config, mode):
340
-        """Run WsgiDAV using cherrypy.wsgiserver, if CherryPy is installed."""
341
-        assert mode in ("cherrypy", "cherrypy-bundled")
342
-
343
-        try:
344
-            from wsgidav.server.cherrypy import wsgiserver
431
+        self._runCherryPy(app, self.config)
345 432
 
346
-            version = "WsgiDAV/%s %s Python/%s" % (
347
-                __version__,
348
-                wsgiserver.CherryPyWSGIServer.version,
349
-                PYTHON_VERSION)
433
+    def _runCherryPy(self, app, config):
434
+        version = "WsgiDAV/%s %s Python/%s" % (
435
+            __version__,
436
+            wsgiserver.CherryPyWSGIServer.version,
437
+            PYTHON_VERSION
438
+        )
350 439
 
351
-            wsgiserver.CherryPyWSGIServer.version = version
440
+        wsgiserver.CherryPyWSGIServer.version = version
352 441
 
353
-            protocol = "http"
442
+        protocol = "http"
354 443
 
355
-            if config["verbose"] >= 1:
356
-                print("Running %s" % version)
357
-                print("Listening on %s://%s:%s ..." % (protocol, config["host"], config["port"]))
358
-            self._server = wsgiserver.CherryPyWSGIServer(
359
-                (config["host"], config["port"]),
360
-                app,
361
-                server_name=version,
362
-            )
444
+        if config["verbose"] >= 1:
445
+            print("Running %s" % version)
446
+            print("Listening on %s://%s:%s ..." % (protocol, config["host"], config["port"]))
447
+        self._server = CherryPyWSGIServer(
448
+            (config["host"], config["port"]),
449
+            app,
450
+            server_name=version,
451
+        )
363 452
 
364
-            self._server.start()
365
-        except ImportError as e:
366
-            if config["verbose"] >= 1:
367
-                print("Could not import wsgiserver.CherryPyWSGIServer.")
368
-            return False
369
-        return True
453
+        self._server.start()
370 454
 
371 455
     def stop(self):
372 456
         self._server.stop()

+ 3 - 1
tracim/tracim/lib/webdav/sql_resources.py View File

@@ -72,7 +72,9 @@ class ManageActions(object):
72 72
         # thus we want to rename a file from 'file.txt' to 'file - deleted... .txt' and not 'file.txt - deleted...'
73 73
         is_file_name = self.content.label == ''
74 74
         if is_file_name:
75
-            extension = re.search(r'(\.[^.]+)$', new_name).group(0)
75
+            search = re.search(r'(\.[^.]+)$', new_name)
76
+            if search:
77
+                extension = search.group(0)
76 78
             new_name = re.sub(r'(\.[^.]+)$', '', new_name)
77 79
 
78 80
         if self._type in [ActionDescription.ARCHIVING, ActionDescription.DELETION]: