Skip to content

Commit 6a3b63c

Browse files
authored
Merge pull request #755 from AnswerDotAI/erikgaas/x_forward_redirect
support x-forwarded-host as source for redirect
2 parents 1866fad + c544ee3 commit 6a3b63c

File tree

3 files changed

+110
-15
lines changed

3 files changed

+110
-15
lines changed

fasthtml/_modidx.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@
197197
'fasthtml/oauth.py'),
198198
'fasthtml.oauth._AppClient.retr_id': ('api/oauth.html#_appclient.retr_id', 'fasthtml/oauth.py'),
199199
'fasthtml.oauth._AppClient.retr_info': ('api/oauth.html#_appclient.retr_info', 'fasthtml/oauth.py'),
200+
'fasthtml.oauth.get_host': ('api/oauth.html#get_host', 'fasthtml/oauth.py'),
200201
'fasthtml.oauth.load_creds': ('api/oauth.html#load_creds', 'fasthtml/oauth.py'),
201202
'fasthtml.oauth.redir_url': ('api/oauth.html#redir_url', 'fasthtml/oauth.py'),
202203
'fasthtml.oauth.url_match': ('api/oauth.html#url_match', 'fasthtml/oauth.py')},

fasthtml/oauth.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
# %% auto 0
66
__all__ = ['http_patterns', 'GoogleAppClient', 'GitHubAppClient', 'HuggingFaceClient', 'DiscordAppClient', 'Auth0AppClient',
7-
'redir_url', 'url_match', 'OAuth', 'load_creds']
7+
'get_host', 'redir_url', 'url_match', 'OAuth', 'load_creds']
88

99
# %% ../nbs/api/08_oauth.ipynb
1010
from .common import *
@@ -121,10 +121,17 @@ def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None,
121121
return self.prepare_request_uri(self.base_url, redirect_uri, scope, state=state, **kwargs)
122122

123123
# %% ../nbs/api/08_oauth.ipynb
124-
def redir_url(request, redir_path, scheme=None):
124+
def get_host(request):
125+
"""Get the host, preferring X-Forwarded-Host if available"""
126+
forwarded_host = request.headers.get('x-forwarded-host')
127+
return forwarded_host if forwarded_host else request.url.netloc
128+
129+
# %% ../nbs/api/08_oauth.ipynb
130+
def redir_url(req, redir_path, scheme=None):
125131
"Get the redir url for the host in `request`"
126-
scheme = 'http' if request.url.hostname in ("localhost", "127.0.0.1") else 'https'
127-
return f"{scheme}://{request.url.netloc}{redir_path}"
132+
host = get_host(req)
133+
scheme = 'http' if host.split(':')[0] in ("localhost", "127.0.0.1") else 'https'
134+
return f"{scheme}://{host}{redir_path}"
128135

129136
# %% ../nbs/api/08_oauth.ipynb
130137
@patch
@@ -159,8 +166,8 @@ def retr_id(self:_AppClient, code, redirect_uri):
159166

160167
# %% ../nbs/api/08_oauth.ipynb
161168
http_patterns = (r'^(localhost|127\.0\.0\.1)(:\d+)?$',)
162-
def url_match(url, patterns=http_patterns):
163-
return any(re.match(pattern, url.netloc.split(':')[0]) for pattern in patterns)
169+
def url_match(request, patterns=http_patterns):
170+
return any(re.match(pattern, get_host(request).split(':')[0]) for pattern in patterns)
164171

165172
# %% ../nbs/api/08_oauth.ipynb
166173
class OAuth:
@@ -178,8 +185,8 @@ def before(req, session):
178185
@app.get(redir_path)
179186
def redirect(req, session, code:str=None, error:str=None, state:str=None):
180187
if not code: session['oauth_error']=error; return RedirectResponse(self.error_path, status_code=303)
181-
scheme = 'http' if url_match(req.url,self.http_patterns) or not self.https else 'https'
182-
base_url = f"{scheme}://{req.url.netloc}"
188+
scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'
189+
base_url = f"{scheme}://{get_host(req)}"
183190
info = AttrDictDefault(cli.retr_info(code, base_url+redir_path))
184191
ident = info.get(self.cli.id_key)
185192
if not ident: return self.redir_login(session)

nbs/api/08_oauth.ipynb

Lines changed: 94 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,49 @@
301301
"server = JupyUvi(app, port=port)"
302302
]
303303
},
304+
{
305+
"cell_type": "code",
306+
"execution_count": null,
307+
"id": "551454f9",
308+
"metadata": {},
309+
"outputs": [],
310+
"source": [
311+
"#|export\n",
312+
"def get_host(request):\n",
313+
" \"\"\"Get the host, preferring X-Forwarded-Host if available\"\"\"\n",
314+
" forwarded_host = request.headers.get('x-forwarded-host')\n",
315+
" return forwarded_host if forwarded_host else request.url.netloc"
316+
]
317+
},
318+
{
319+
"cell_type": "code",
320+
"execution_count": null,
321+
"id": "3cc9b978",
322+
"metadata": {},
323+
"outputs": [
324+
{
325+
"name": "stdout",
326+
"output_type": "stream",
327+
"text": [
328+
"Without X-Forwarded-Host: localhost:8000\n",
329+
"With X-Forwarded-Host: example.com\n"
330+
]
331+
}
332+
],
333+
"source": [
334+
"from types import SimpleNamespace\n",
335+
"from urllib.parse import urlparse\n",
336+
"\n",
337+
"mock_request_localhost = SimpleNamespace(headers={}, url=SimpleNamespace(netloc='localhost:8000'))\n",
338+
"mock_request_with_forward = SimpleNamespace(\n",
339+
" headers={'x-forwarded-host': 'example.com'}, \n",
340+
" url=SimpleNamespace(netloc='localhost:8000', hostname='localhost')\n",
341+
")\n",
342+
"\n",
343+
"print(\"Without X-Forwarded-Host:\", get_host(mock_request_localhost))\n",
344+
"print(\"With X-Forwarded-Host:\", get_host(mock_request_with_forward))"
345+
]
346+
},
304347
{
305348
"cell_type": "code",
306349
"execution_count": null,
@@ -309,10 +352,11 @@
309352
"outputs": [],
310353
"source": [
311354
"#| export\n",
312-
"def redir_url(request, redir_path, scheme=None):\n",
355+
"def redir_url(req, redir_path, scheme=None):\n",
313356
" \"Get the redir url for the host in `request`\"\n",
314-
" scheme = 'http' if request.url.hostname in (\"localhost\", \"127.0.0.1\") else 'https'\n",
315-
" return f\"{scheme}://{request.url.netloc}{redir_path}\""
357+
" host = get_host(req)\n",
358+
" scheme = 'http' if host.split(':')[0] in (\"localhost\", \"127.0.0.1\") else 'https'\n",
359+
" return f\"{scheme}://{host}{redir_path}\""
316360
]
317361
},
318362
{
@@ -447,8 +491,35 @@
447491
"source": [
448492
"#| export\n",
449493
"http_patterns = (r'^(localhost|127\\.0\\.0\\.1)(:\\d+)?$',)\n",
450-
"def url_match(url, patterns=http_patterns):\n",
451-
" return any(re.match(pattern, url.netloc.split(':')[0]) for pattern in patterns)"
494+
"def url_match(request, patterns=http_patterns):\n",
495+
" return any(re.match(pattern, get_host(request).split(':')[0]) for pattern in patterns)"
496+
]
497+
},
498+
{
499+
"cell_type": "code",
500+
"execution_count": null,
501+
"id": "fee2db6c",
502+
"metadata": {},
503+
"outputs": [
504+
{
505+
"name": "stdout",
506+
"output_type": "stream",
507+
"text": [
508+
"Localhost: http://localhost:8000/redirect\n",
509+
"With X-Forwarded-Host: https://example.com/redirect\n",
510+
"Production: https://myapp.com/redirect\n"
511+
]
512+
}
513+
],
514+
"source": [
515+
"from types import SimpleNamespace\n",
516+
"from urllib.parse import urlparse\n",
517+
"\n",
518+
"mock_request_prod = SimpleNamespace(headers={}, url=SimpleNamespace(netloc='myapp.com', hostname='myapp.com'))\n",
519+
"\n",
520+
"print(\"Localhost:\", redir_url(mock_request_localhost, '/redirect'))\n",
521+
"print(\"With X-Forwarded-Host:\", redir_url(mock_request_with_forward, '/redirect'))\n",
522+
"print(\"Production:\", redir_url(mock_request_prod, '/redirect'))"
452523
]
453524
},
454525
{
@@ -474,8 +545,8 @@
474545
" @app.get(redir_path)\n",
475546
" def redirect(req, session, code:str=None, error:str=None, state:str=None):\n",
476547
" if not code: session['oauth_error']=error; return RedirectResponse(self.error_path, status_code=303)\n",
477-
" scheme = 'http' if url_match(req.url,self.http_patterns) or not self.https else 'https'\n",
478-
" base_url = f\"{scheme}://{req.url.netloc}\"\n",
548+
" scheme = 'http' if url_match(req,self.http_patterns) or not self.https else 'https'\n",
549+
" base_url = f\"{scheme}://{get_host(req)}\"\n",
479550
" info = AttrDictDefault(cli.retr_info(code, base_url+redir_path))\n",
480551
" ident = info.get(self.cli.id_key)\n",
481552
" if not ident: return self.redir_login(session)\n",
@@ -551,6 +622,8 @@
551622
"text/markdown": [
552623
"---\n",
553624
"\n",
625+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L223){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
626+
"\n",
554627
"### GoogleAppClient.consent_url\n",
555628
"\n",
556629
"> GoogleAppClient.consent_url (proj=None)\n",
@@ -560,6 +633,8 @@
560633
"text/plain": [
561634
"---\n",
562635
"\n",
636+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L223){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
637+
"\n",
563638
"### GoogleAppClient.consent_url\n",
564639
"\n",
565640
"> GoogleAppClient.consent_url (proj=None)\n",
@@ -602,6 +677,8 @@
602677
"text/markdown": [
603678
"---\n",
604679
"\n",
680+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L231){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
681+
"\n",
605682
"### Credentials.update\n",
606683
"\n",
607684
"> Credentials.update ()\n",
@@ -611,6 +688,8 @@
611688
"text/plain": [
612689
"---\n",
613690
"\n",
691+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L231){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
692+
"\n",
614693
"### Credentials.update\n",
615694
"\n",
616695
"> Credentials.update ()\n",
@@ -652,6 +731,8 @@
652731
"text/markdown": [
653732
"---\n",
654733
"\n",
734+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L238){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
735+
"\n",
655736
"### Credentials.save\n",
656737
"\n",
657738
"> Credentials.save (fname)\n",
@@ -661,6 +742,8 @@
661742
"text/plain": [
662743
"---\n",
663744
"\n",
745+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L238){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
746+
"\n",
664747
"### Credentials.save\n",
665748
"\n",
666749
"> Credentials.save (fname)\n",
@@ -717,6 +800,8 @@
717800
"text/markdown": [
718801
"---\n",
719802
"\n",
803+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L249){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
804+
"\n",
720805
"### GoogleAppClient.creds\n",
721806
"\n",
722807
"> GoogleAppClient.creds ()\n",
@@ -726,6 +811,8 @@
726811
"text/plain": [
727812
"---\n",
728813
"\n",
814+
"[source](https://github.com/AnswerDotAI/fasthtml/blob/main/fasthtml/oauth.py#L249){target=\"_blank\" style=\"float:right; font-size:smaller\"}\n",
815+
"\n",
729816
"### GoogleAppClient.creds\n",
730817
"\n",
731818
"> GoogleAppClient.creds ()\n",

0 commit comments

Comments
 (0)