Repository: scrapy/w3lib

Issue:
canonicalize_url inappropriately "lowercases" credentials (username/password) in url
```python
from w3lib.url import canonicalize_url


def main():
    url = "sftp://USERNAME:ABC@domain.com"

    print(canonicalize_url(url))
    print(canonicalize_url(url) == url)

``` 

```
sftp://username:abc@domain.com/
False
```

Hints from issue comments:
@john-parton: This is where the transformation takes place: https://github.com/scrapy/w3lib/blob/d7c3307f8f7e30cdc91d48987e57c42ca04a5df3/w3lib/url.py#L659

If you remove the `lower()` then tests fail, `test_domains_are_case_insensitive`@john-parton: Thanks so much for the fix! 

Solution patch:
diff --git a/w3lib/url.py b/w3lib/url.py
index 28e70cb..c142048 100644
--- a/w3lib/url.py
+++ b/w3lib/url.py
@@ -654,10 +654,13 @@ def canonicalize_url(
 
     fragment = "" if not keep_fragments else fragment
 
+    # Apply lowercase to the domain, but not to the userinfo.
+    netloc_parts = netloc.split("@")
+    netloc_parts[-1] = netloc_parts[-1].lower().rstrip(":")
+    netloc = "@".join(netloc_parts)
+
     # every part should be safe already
-    return urlunparse(
-        (scheme, netloc.lower().rstrip(":"), path, params, query, fragment)
-    )
+    return urlunparse((scheme, netloc, path, params, query, fragment))
 
 
 def _unquotepath(path: str) -> bytes:


Test patch:
diff --git a/tests/test_url.py b/tests/test_url.py
index ca84745..319d76c 100644
--- a/tests/test_url.py
+++ b/tests/test_url.py
@@ -1384,6 +1384,12 @@ class CanonicalizeUrlTest(unittest.TestCase):
             canonicalize_url("http://www.EXAMPLE.com/"), "http://www.example.com/"
         )
 
+    def test_userinfo_is_case_sensitive(self):
+        self.assertEqual(
+            canonicalize_url("sftp://UsEr:PaSsWoRd@www.EXAMPLE.com/"),
+            "sftp://UsEr:PaSsWoRd@www.example.com/",
+        )
+
     def test_canonicalize_idns(self):
         self.assertEqual(
             canonicalize_url("http://www.bücher.de?q=bücher"),