Skip to content

Commit 094d386

Browse files
namsnathgaurpulkit
andauthored
fix: makes url path normalization case sensitive (supertokens#565)
- Updates `normalise_url_path_or_throw_error` to be case sensitive - URL paths will not be converted to lower-case, and will be kept as-is. Co-authored-by: Pulkit Gaur <gaurpulkit99@gmail.com>
1 parent 6195a78 commit 094d386

File tree

4 files changed

+107
-120
lines changed

4 files changed

+107
-120
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [unreleased]
1010

1111
## [0.29.0] - 2025-03-03
12+
### Breaking changes
13+
- Makes URL path normalization case sensitive
14+
- Updates `normalise_url_path_or_throw_error` to be case sensitive
15+
- URL paths will not be converted to lower-case, and will be kept as-is.
16+
17+
### Changes
1218
- Adds option to disable `tldextract` HTTP calls by setting `SUPERTOKENS_TLDEXTRACT_DISABLE_HTTP=1`
19+
20+
### Infrastructure
1321
- Upgrades `pip` and `setuptools` in CI publish job
1422
- Also upgrades `poetry` and it's dependency - `clikit`
1523
- Migrates unit tests to use a containerized core
1624
- Updates `Makefile` to use a Docker `compose` setup step
1725
- Migrates unit tests from CircleCI to Github Actions
1826
- Adds lint/format checks to Github Actions
1927

28+
2029
## [0.28.1] - 2025-02-26
2130
- Pins `httpx` and `respx` to current major versions (<1.0.0)
2231
- Removes `respx` dependency from `fastapi` install

supertokens_python/normalised_url_path.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,32 +45,35 @@ def is_a_recipe_path(self) -> bool:
4545

4646

4747
def normalise_url_path_or_throw_error(input_str: str) -> str:
48-
input_str = input_str.strip().lower()
48+
input_str = input_str.strip()
49+
input_str_lower = input_str.lower()
50+
4951
try:
50-
if not input_str.startswith("http://") and not input_str.startswith("https://"):
52+
if not input_str_lower.startswith(("http://", "https://")):
5153
raise Exception("converting to proper URL")
54+
5255
url_obj = urlparse(input_str)
53-
input_str = url_obj.path
54-
if input_str.endswith("/"):
55-
return input_str[:-1]
56-
return input_str
56+
url_path = url_obj.path
57+
58+
if url_path.endswith("/"):
59+
return url_path[:-1]
60+
61+
return url_path
5762
except Exception:
5863
pass
5964

6065
if (
61-
(domain_given(input_str) or input_str.startswith("localhost"))
62-
and not input_str.startswith("http://")
63-
and not input_str.startswith("https://")
64-
):
66+
domain_given(input_str_lower) or input_str_lower.startswith("localhost")
67+
) and not input_str_lower.startswith(("http://", "https://")):
6568
input_str = "http://" + input_str
6669
return normalise_url_path_or_throw_error(input_str)
6770

6871
if not input_str.startswith("/"):
6972
input_str = "/" + input_str
7073

7174
try:
72-
urlparse("http://example.com" + input_str)
73-
return normalise_url_path_or_throw_error("http://example.com" + input_str)
75+
urlparse(f"http://example.com{input_str}")
76+
return normalise_url_path_or_throw_error(f"http://example.com{input_str}")
7477
except Exception:
7578
raise_general_exception("Please provide a valid URL path")
7679

tests/test_config.py

Lines changed: 64 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
14-
from typing import Any, Dict, Optional
1514
from unittest.mock import MagicMock
1615

1716
import pytest
@@ -24,8 +23,9 @@
2423
from supertokens_python.recipe.session import SessionRecipe
2524
from supertokens_python.recipe.session.asyncio import create_new_session
2625
from supertokens_python.types import RecipeUserId
26+
from typing_extensions import Any, Dict, Optional
2727

28-
from tests.utils import get_new_core_app_url, reset
28+
from tests.utils import get_new_core_app_url, outputs, reset
2929

3030

3131
# Tests do not rely on the core.
@@ -35,109 +35,73 @@ def st_config() -> SupertokensConfig:
3535
return SupertokensConfig(get_new_core_app_url())
3636

3737

38-
def testing_URL_path_normalisation():
38+
@mark.parametrize(
39+
("input", "expectation"),
40+
[
41+
("exists?email=john.doe%40gmail.com", outputs("/exists")),
42+
(
43+
"/auth/email/exists?email=john.doe%40gmail.com",
44+
outputs("/auth/email/exists"),
45+
),
46+
("http://api.example.com", outputs("")),
47+
("https://api.example.com", outputs("")),
48+
("http://api.example.com?hello=1", outputs("")),
49+
("http://api.example.com/hello", outputs("/hello")),
50+
("http://api.example.com/HellO", outputs("/HellO")),
51+
("http://api.example.com/", outputs("")),
52+
("http://api.example.com:8080", outputs("")),
53+
("api.example.com/", outputs("")),
54+
("api.example.com#random", outputs("")),
55+
(".example.com", outputs("")),
56+
("api.example.com/?hello=1&bye=2", outputs("")),
57+
("exists", outputs("/exists")),
58+
("eXiStS", outputs("/eXiStS")),
59+
("/exists", outputs("/exists")),
60+
("/eXiStS", outputs("/eXiStS")),
61+
("/exists?email=john.doe%40gmail.com", outputs("/exists")),
62+
("http://api.example.com/one/two", outputs("/one/two")),
63+
("http://1.2.3.4/one/two", outputs("/one/two")),
64+
("1.2.3.4/one/two", outputs("/one/two")),
65+
("https://api.example.com/one/two/", outputs("/one/two")),
66+
("http://api.example.com/one/two?hello=1", outputs("/one/two")),
67+
("http://api.example.com/hello/", outputs("/hello")),
68+
("http://api.example.com/one/two/", outputs("/one/two")),
69+
("http://api.example.com/one/two#random2", outputs("/one/two")),
70+
("api.example.com/one/two", outputs("/one/two")),
71+
(".example.com/one/two", outputs("/one/two")),
72+
("api.example.com/one/two?hello=1&bye=2", outputs("/one/two")),
73+
("/one/two", outputs("/one/two")),
74+
("one/two", outputs("/one/two")),
75+
("one/two/", outputs("/one/two")),
76+
("/one", outputs("/one")),
77+
("one", outputs("/one")),
78+
("one/", outputs("/one")),
79+
("/one/two/", outputs("/one/two")),
80+
("/one/two?hello=1", outputs("/one/two")),
81+
("one/two?hello=1", outputs("/one/two")),
82+
("/one/two/#randm,", outputs("/one/two")),
83+
("one/two#random", outputs("/one/two")),
84+
("localhost:4000/one/two", outputs("/one/two")),
85+
("127.0.0.1:4000/one/two", outputs("/one/two")),
86+
("127.0.0.1/one/two", outputs("/one/two")),
87+
("https://127.0.0.1:80/one/two", outputs("/one/two")),
88+
("/", outputs("")),
89+
("", outputs("")),
90+
("/.netlify/functions/api", outputs("/.netlify/functions/api")),
91+
("/netlify/.functions/api", outputs("/netlify/.functions/api")),
92+
("app.example.com/.netlify/functions/api", outputs("/.netlify/functions/api")),
93+
("app.example.com/netlify/.functions/api", outputs("/netlify/.functions/api")),
94+
("/app.example.com", outputs("/app.example.com")),
95+
],
96+
)
97+
def testing_URL_path_normalisation(input: str, expectation: Any) -> None:
3998
def normalise_url_path_or_throw_error(
4099
input: str,
41100
): # pylint: disable=redefined-builtin
42101
return NormalisedURLPath(input).get_as_string_dangerous()
43102

44-
assert (
45-
normalise_url_path_or_throw_error("exists?email=john.doe%40gmail.com")
46-
== "/exists"
47-
)
48-
assert (
49-
normalise_url_path_or_throw_error(
50-
"/auth/email/exists?email=john.doe%40gmail.com"
51-
)
52-
== "/auth/email/exists"
53-
)
54-
assert normalise_url_path_or_throw_error("exists") == "/exists"
55-
assert normalise_url_path_or_throw_error("/exists") == "/exists"
56-
assert (
57-
normalise_url_path_or_throw_error("/exists?email=john.doe%40gmail.com")
58-
== "/exists"
59-
)
60-
assert normalise_url_path_or_throw_error("http://api.example.com") == ""
61-
assert normalise_url_path_or_throw_error("https://api.example.com") == ""
62-
assert normalise_url_path_or_throw_error("http://api.example.com?hello=1") == ""
63-
assert normalise_url_path_or_throw_error("http://api.example.com/hello") == "/hello"
64-
assert normalise_url_path_or_throw_error("http://api.example.com/") == ""
65-
assert normalise_url_path_or_throw_error("http://api.example.com:8080") == ""
66-
assert normalise_url_path_or_throw_error("api.example.com/") == ""
67-
assert normalise_url_path_or_throw_error("api.example.com#random") == ""
68-
assert normalise_url_path_or_throw_error(".example.com") == ""
69-
assert normalise_url_path_or_throw_error("api.example.com/?hello=1&bye=2") == ""
70-
71-
assert (
72-
normalise_url_path_or_throw_error("http://api.example.com/one/two")
73-
== "/one/two"
74-
)
75-
assert normalise_url_path_or_throw_error("http://1.2.3.4/one/two") == "/one/two"
76-
assert normalise_url_path_or_throw_error("1.2.3.4/one/two") == "/one/two"
77-
assert (
78-
normalise_url_path_or_throw_error("https://api.example.com/one/two/")
79-
== "/one/two"
80-
)
81-
assert (
82-
normalise_url_path_or_throw_error("http://api.example.com/one/two?hello=1")
83-
== "/one/two"
84-
)
85-
assert (
86-
normalise_url_path_or_throw_error("http://api.example.com/hello/") == "/hello"
87-
)
88-
assert (
89-
normalise_url_path_or_throw_error("http://api.example.com/one/two/")
90-
== "/one/two"
91-
)
92-
assert (
93-
normalise_url_path_or_throw_error("http://api.example.com/one/two#random2")
94-
== "/one/two"
95-
)
96-
assert normalise_url_path_or_throw_error("api.example.com/one/two") == "/one/two"
97-
assert normalise_url_path_or_throw_error(".example.com/one/two") == "/one/two"
98-
assert (
99-
normalise_url_path_or_throw_error("api.example.com/one/two?hello=1&bye=2")
100-
== "/one/two"
101-
)
102-
103-
assert normalise_url_path_or_throw_error("/one/two") == "/one/two"
104-
assert normalise_url_path_or_throw_error("one/two") == "/one/two"
105-
assert normalise_url_path_or_throw_error("one/two/") == "/one/two"
106-
assert normalise_url_path_or_throw_error("/one") == "/one"
107-
assert normalise_url_path_or_throw_error("one") == "/one"
108-
assert normalise_url_path_or_throw_error("one/") == "/one"
109-
assert normalise_url_path_or_throw_error("/one/two/") == "/one/two"
110-
assert normalise_url_path_or_throw_error("/one/two?hello=1") == "/one/two"
111-
assert normalise_url_path_or_throw_error("one/two?hello=1") == "/one/two"
112-
assert normalise_url_path_or_throw_error("/one/two/#randm,") == "/one/two"
113-
assert normalise_url_path_or_throw_error("one/two#random") == "/one/two"
114-
115-
assert normalise_url_path_or_throw_error("localhost:4000/one/two") == "/one/two"
116-
assert normalise_url_path_or_throw_error("127.0.0.1:4000/one/two") == "/one/two"
117-
assert normalise_url_path_or_throw_error("127.0.0.1/one/two") == "/one/two"
118-
assert (
119-
normalise_url_path_or_throw_error("https://127.0.0.1:80/one/two") == "/one/two"
120-
)
121-
assert normalise_url_path_or_throw_error("/") == ""
122-
assert normalise_url_path_or_throw_error("") == ""
123-
124-
assert (
125-
normalise_url_path_or_throw_error("/.netlify/functions/api")
126-
== "/.netlify/functions/api"
127-
)
128-
assert (
129-
normalise_url_path_or_throw_error("/netlify/.functions/api")
130-
== "/netlify/.functions/api"
131-
)
132-
assert (
133-
normalise_url_path_or_throw_error("app.example.com/.netlify/functions/api")
134-
== "/.netlify/functions/api"
135-
)
136-
assert (
137-
normalise_url_path_or_throw_error("app.example.com/netlify/.functions/api")
138-
== "/netlify/.functions/api"
139-
)
140-
assert normalise_url_path_or_throw_error("/app.example.com") == "/app.example.com"
103+
with expectation as output:
104+
assert normalise_url_path_or_throw_error(input) == output
141105

142106

143107
def testing_URL_domain_normalisation():

tests/utils.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -491,14 +491,25 @@ async def create_users(
491491

492492

493493
@contextmanager
494-
def outputs(val: Any):
494+
def outputs(value: Any):
495495
"""
496496
Outputs a value to assert.
497-
498-
Usage:
499-
@mark.parametrize(["input", "expectation"], [(1, outputs(1)), (0, raises(Exception))])
500-
def test_fn(input, expectation):
501-
with expectation as expected_output:
502-
assert 1 / input == expected_output
497+
Can be used for a common interface in test parameters.
498+
499+
Example:
500+
```python
501+
@mark.parametrize(
502+
("input", "expectation"),
503+
[
504+
(1, outputs(1)),
505+
(0, raises(Exception)),
506+
]
507+
)
508+
def test(input, expectation):
509+
# In case of exceptions, the `raises` will catch it
510+
# In normal execution, the `expected_output` contains the assertion value
511+
with expectation as expected_output:
512+
assert 1 / input == expected_output
513+
```
503514
"""
504-
yield val
515+
yield value

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy