-
-
Notifications
You must be signed in to change notification settings - Fork 962
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix Host based routing to check path #2263
Fix Host based routing to check path #2263
Conversation
fab3fdc
to
edac97d
Compare
edac97d
to
343f735
Compare
343f735
to
2f24fea
Compare
2f24fea
to
fdd7d43
Compare
Tested and can confirm this solves #2248 |
65f9b08
to
6f80445
Compare
Any ETA on merge / next release, @Kludex? 🙂 |
No. You are not blocked to have this on your project. Subclass |
Thank you for the feedback, and yes I am aware. That is what I am currently doing, but just looking forward to see it added in so I can remove my local patch/override 👍 |
Good thing this was not merged in yet, because I just found another bug while testing. It seems the same issue occurs for Here is a modified example to test with: # example.py
from starlette.applications import Starlette
from starlette.routing import Host, Mount, Route, Router, Match as M
from starlette.responses import PlainTextResponse
import uvicorn
# -----------------------------------------------------------------------------------
from starlette.datastructures import Headers
from starlette.types import Scope
import typing
class Host(Host):
# Overrides matches as according to PR: https://github.com/encode/starlette/pull/2263
def matches(self, scope: Scope) -> typing.Tuple[M, Scope]:
if scope["type"] in ("http", "websocket"):
headers = Headers(scope=scope)
host = headers.get("host", "").split(":")[0]
host_match = self.host_regex.match(host)
path_match = self.routes == [] or any(
[route.matches(scope)[0] == M.FULL for route in self.routes]
)
if host_match and path_match:
matched_params = host_match.groupdict()
for key, value in matched_params.items():
matched_params[key] = self.param_convertors[key].convert(value)
path_params = dict(scope.get("path_params", {}))
path_params.update(matched_params)
child_scope = {"path_params": path_params, "endpoint": self.app}
return M.FULL, child_scope
return M.NONE, {}
# -----------------------------------------------------------------------------------
async def home(request):
return PlainTextResponse("home\n")
async def foo(request):
return PlainTextResponse("foo\n")
async def bar(request):
return PlainTextResponse("bar\n")
ROUTES = [
Route("/", endpoint=home, name="Home"),
Host("api.example.com", name="Foo Routes", app=Router(routes=[
#Mount("/foo", name="Foo Mount", routes=[
Mount("/", name="Foo Mount", routes=[ # 200 OK
Route("/foo", foo, name="Foo")
]),
])),
Host("api.example.com", name="Bar Routes", app=Router(routes=[
Mount("/bar", name="Bar Mount", routes=[ # 404 Not Found
Route("/bar", bar, name="Bar")
]),
])),
]
for r in ROUTES:
print(r)
app = Starlette(routes=ROUTES)
if __name__ == '__main__':
uvicorn.run("example:app", host='0.0.0.0', port=8000, reload=True)
# Run tests:
# curl -i api.example.com:8000/foo # 200 OK
# curl -i api.example.com:8000/bar/bar # 404 Not found The example above is very similar to the one in #2248 except that patch #2263 is added and the
This can probably be resolved similar to how |
@nicolaipre I don't think this is related to the ROUTES = [
Route("/", endpoint=home, name="Home"),
Mount("/", name="Foo Mount", routes=[
Route("/foo", foo, name="Foo")
]),
Mount("/bar", name="Bar Mount", routes=[ # 404 Not Found
Route("/bar", bar, name="Bar")
]),
] You will see:
Update: This is because the first Mount is more general than the second specific Mount so the first one is tried: https://www.starlette.io/routing/#route-priority |
@aminalaee You are completely right, and thank you for reminding me of routing priority. I copied the routes from your last reply and did some testing. It works fine when the more specific ROUTES = [
Mount("/bar", name="Bar Mount", routes=[ # More specific
Route("/bar", bar, name="Bar")
]),
Mount("/", name="Foo Mount", routes=[ # Less specific
Route("/foo", foo, name="Foo")
]),
] In this case you get Initially I thought this would be the opposite of the example in the documentation, or that it shouldn't really matter since these are only hard-coded routes without variables, so I am still a little confused as to why this happens. Here is a response from a forum for a different framework where someone had a similar issue: In the documentation there is an example of When you use
Sorry if I am misunderstanding here, but I am confused by why this happens. Wouldn't this just be the same as registering the following two routes when resolved? Route("/foo", foo, name="Foo Route"),
Route("/bar/bar", bar, name="Bar Route") What exactly is it that makes When building bigger applications with multiple I think it is good to keep the PR as the proposed fix actually solves the |
This comment was marked as outdated.
This comment was marked as outdated.
@nicolaipre Yes, please start a discussion for this, as it's not entirely related to this issue, we can link the issues for the reference. |
I agree. Started a separate discussion in #2299. Discovered some interesting behavior when testing again. Seems like it is a bug in the routing or something... Edit: proposed fix in PR: #2301 |
This PR makes large sites vulnerable to CC attacks. Imagine a site with hundreds or even thousands of routes, performing hundreds or thousands of regular expression matches at the beginning of each request, which consumes a lot of CPU resources. We have two other solutions:
|
I think I've mentioned this before, but I agree with adding it as a limitation in the docs, the same way route priority is done https://www.starlette.io/routing/#route-priority |
Let's go with the documentation, then. 🙏 |
Summary
Closes #2248
Checklist