diff --git a/scrapy/extensions/feedexport.py b/scrapy/extensions/feedexport.py index d35551fdd..ce2846eba 100644 --- a/scrapy/extensions/feedexport.py +++ b/scrapy/extensions/feedexport.py @@ -11,7 +11,7 @@ import posixpath from tempfile import NamedTemporaryFile from datetime import datetime import six -from six.moves.urllib.parse import urlparse +from six.moves.urllib.parse import urlparse, unquote from ftplib import FTP from zope.interface import Interface, implementer @@ -162,7 +162,7 @@ class FTPFeedStorage(BlockingFeedStorage): self.host = u.hostname self.port = int(u.port or '21') self.username = u.username - self.password = u.password + self.password = unquote(u.password) self.path = u.path self.use_active_mode = use_active_mode diff --git a/tests/test_feedexport.py b/tests/test_feedexport.py index c5063253a..f32ac2a4b 100644 --- a/tests/test_feedexport.py +++ b/tests/test_feedexport.py @@ -6,7 +6,8 @@ import warnings from io import BytesIO import tempfile import shutil -from six.moves.urllib.parse import urljoin, urlparse +import string +from six.moves.urllib.parse import urljoin, urlparse, quote from six.moves.urllib.request import pathname2url from zope.interface.verify import verifyObject @@ -98,6 +99,12 @@ class FTPFeedStorageTest(unittest.TestCase): verifyObject(IFeedStorage, st) return self._assert_stores(st, path) + def test_uri_auth_quote(self): + # RFC3986: 3.2.1. User Information + pw_quoted = quote(string.punctuation, safe='') + st = FTPFeedStorage('ftp://foo:%s@example.com/some_path' % pw_quoted) + self.assertEqual(st.password, string.punctuation) + @defer.inlineCallbacks def _assert_stores(self, storage, path): spider = self.get_test_spider()