Author: johnnyg

Revision: 5891

Log:
        Use filename suggested by content-disposition header.
Closes #1040.

Diff:
Modified: trunk/deluge/httpdownloader.py
===================================================================
--- trunk/deluge/httpdownloader.py      2009-10-28 17:43:29 UTC (rev 5890)
+++ trunk/deluge/httpdownloader.py      2009-10-29 06:02:20 UTC (rev 5891)
@@ -36,13 +36,15 @@
 from twisted.web.error import PageRedirect
 from twisted.python.failure import Failure
 from twisted.internet import reactor
+from deluge.log import setupLogger, LOG as log
 from common import get_version
+import os.path
 
 class HTTPDownloader(client.HTTPDownloader):
     """
     Factory class for downloading files and keeping track of progress.
     """
-    def __init__(self, url, filename, part_callback=None, headers=None):
+    def __init__(self, url, filename, part_callback=None, headers=None, 
force_filename=False):
         """
         :param url: the url to download from
         :type url: string
@@ -57,6 +59,7 @@
         self.__part_callback = part_callback
         self.current_length = 0
         self.value = filename
+        self.force_filename = force_filename
         agent = "Deluge/%s (http://deluge-torrent.org)" % get_version()
         client.HTTPDownloader.__init__(self, url, filename, headers=headers, 
agent=agent)
 
@@ -70,6 +73,18 @@
                 self.total_length = int(headers["content-length"][0])
             else:
                 self.total_length = 0
+
+            if "content-disposition" in headers and not self.force_filename:
+                try:
+                    new_file_name = 
str(headers["content-disposition"][0]).split(";")[1].split("=")[1]
+                    new_file_name = sanitise_filename(new_file_name)
+                    new_file_name = 
os.path.join(os.path.split(self.fileName)[0], new_file_name)
+                except Exception, e:
+                    log.exception(e)
+                else:
+                    self.fileName = new_file_name
+                    self.value = new_file_name
+
         elif self.code in (http.TEMPORARY_REDIRECT, http.MOVED_PERMANENTLY):
             location = headers["location"][0]
             error = PageRedirect(self.code, location=location)
@@ -85,8 +100,40 @@
 
         return client.HTTPDownloader.pagePart(self, data)
 
-def download_file(url, filename, callback=None, headers=None):
+def sanitise_filename(filename):
     """
+    Sanitises a filename to use as a download destination file.
+    Logs any filenames that could be considered malicious.
+
+    :param filename: the filename to sanitise
+    :type filename: string
+    :returns: the sanitised filename
+    :rtype: string
+
+    :raises IOError: when the filename exists
+    """
+
+    # Remove any quotes
+    filename = filename.strip("'\"")
+
+    if os.path.basename(filename) != filename:
+        # Dodgy server, log it
+        log.warning("Potentially malicious server: trying to write to file 
'%s'" % filename)
+        # Only use the basename
+        filename = os.path.basename(filename)
+        
+    filename = filename.strip()
+    if filename.startswith(".") or ";" in filename or "|" in filename:
+        # Dodgy server, log it
+        log.warning("Potentially malicious server: trying to write to file 
'%s'" % filename)
+
+    if os.path.exists(filename):
+        raise IOError, "File '%s' already exists!" % filename
+
+    return filename
+
+def download_file(url, filename, callback=None, headers=None, 
force_filename=False):
+    """
     Downloads a file from a specific URL and returns a Deferred.  You can also
     specify a callback function to be called as parts are received.
 
@@ -99,6 +146,9 @@
     :type callback: function
     :param headers: any optional headers to send
     :type headers: dictionary
+    :param force_filename: force us to use the filename specified rather than
+                           one the server may suggest
+    :type force_filename: boolean
 
     :returns: the filename of the downloaded file
     :rtype: Deferred
@@ -114,7 +164,7 @@
             headers[str(key)] = str(value)
 
     scheme, host, port, path = client._parse(url)
-    factory = HTTPDownloader(url, filename, callback, headers)
+    factory = HTTPDownloader(url, filename, callback, headers, force_filename)
     if scheme == "https":
         from twisted.internet import ssl
         reactor.connectSSL(host, port, factory, ssl.ClientContextFactory())

Modified: trunk/tests/test_httpdownloader.py
===================================================================
--- trunk/tests/test_httpdownloader.py  2009-10-28 17:43:29 UTC (rev 5890)
+++ trunk/tests/test_httpdownloader.py  2009-10-29 06:02:20 UTC (rev 5891)
@@ -2,25 +2,81 @@
 from twisted.python.failure import Failure
 
 from deluge.httpdownloader import download_file
+from deluge.log import setupLogger
 
 class DownloadFileTestCase(unittest.TestCase):
+    def setUp(self):
+        setupLogger("warning", "log_file")
+
+    def tearDown(self):
+        pass
+
+    def assertContains(self, filename, contents):
+        with open(filename) as f:
+            self.assertEqual(f.read(), contents)
+        return filename
+
     def test_download(self):
         d = download_file("http://deluge-torrent.org";, "index.html")
         d.addCallback(self.assertEqual, "index.html")
         return d
 
-    def test_download_with_cookies(self):
-        pass
+    def test_download_without_required_cookies(self):
+        url = "http://deluge-torrent.org/httpdownloader.php?test=cookie";
+        d = download_file(url, "none")
+        d.addCallback(self.fail)
+        d.addErrback(self.assertIsInstance, Failure)
+        return d
 
-    def test_page_moved(self):
-        pass
+    def test_download_with_required_cookies(self):
+        url = "http://deluge-torrent.org/httpdownloader.php?test=cookie";
+        cookie = { "cookie" : "password=deluge" }
+        d = download_file(url, "monster", headers=cookie)
+        d.addCallback(self.assertEqual, "monster")
+        d.addCallback(self.assertContains, "COOKIE MONSTER!")
+        return d
 
-    def test_page_moved_permanently(self):
-        pass
+    def test_download_with_rename(self):
+        url = 
"http://deluge-torrent.org/httpdownloader.php?test=rename&filename=renamed";
+        d = download_file(url, "original")
+        d.addCallback(self.assertEqual, "renamed")
+        d.addCallback(self.assertContains, "This file should be called 
renamed")
+        return d
 
-    def test_page_not_modified(self):
-        pass
+    def test_download_with_rename_fail(self):
+        url = 
"http://deluge-torrent.org/httpdownloader.php?test=rename&filename=renamed";
+        d = download_file(url, "original")
+        d.addCallback(self.assertEqual, "original")
+        d.addCallback(self.assertContains, "This file should be called 
renamed")
+        return d
 
+    def test_download_with_rename_sanitised(self):
+        url = 
"http://deluge-torrent.org/httpdownloader.php?test=rename&filename=/etc/passwd";
+        d = download_file(url, "original")
+        d.addCallback(self.assertEqual, "passwd")
+        d.addCallback(self.assertContains, "This file should be called 
/etc/passwd")
+        return d
+
+    def test_download_with_rename_prevented(self):
+        url = 
"http://deluge-torrent.org/httpdownloader.php?test=rename&filename=spam";
+        d = download_file(url, "forced", force_filename=True)
+        d.addCallback(self.assertEqual, "forced")
+        d.addCallback(self.assertContains, "This file should be called spam")
+        return d
+
+    def test_download_with_gzip_encoding(self):
+        url = 
"http://deluge-torrent.org/httpdownloader.php?test=gzip&msg=success";
+        d = download_file(url, "gzip_encoded")
+        d.addCallback(self.assertContains, "success")
+        return d
+
+    def test_page_redirect(self):
+        url = "http://deluge-torrent.org/httpdownloader.php?test=redirect";
+        d = download_file(url, "none")
+        d.addCallback(self.fail)
+        d.addErrback(self.assertIsInstance, Failure)
+        return d
+
     def test_page_not_found(self):
         d = download_file("http://does.not.exist";, "none")
         d.addCallback(self.fail)



--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"deluge-commit" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to 
[email protected]
For more options, visit this group at 
http://groups.google.com/group/deluge-commit?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to