Skip to content

Commit 9ee83d7

Browse files
committed
support x-forwarded-host as source for redirect
1 parent 1866fad commit 9ee83d7

File tree

3 files changed

+90
-11
lines changed

3 files changed

+90
-11
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: 13 additions & 6 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 *
@@ -120,11 +120,18 @@ def login_link(self:WebApplicationClient, redirect_uri, scope=None, state=None,
120120
if not state: state=getattr(self, 'state', None)
121121
return self.prepare_request_uri(self.base_url, redirect_uri, scope, state=state, **kwargs)
122122

123+
# %% ../nbs/api/08_oauth.ipynb
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+
123129
# %% ../nbs/api/08_oauth.ipynb
124130
def redir_url(request, 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(request)
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:
@@ -179,7 +186,7 @@ def before(req, session):
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)
181188
scheme = 'http' if url_match(req.url,self.http_patterns) or not self.https else 'https'
182-
base_url = f"{scheme}://{req.url.netloc}"
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: 76 additions & 5 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,
@@ -311,8 +354,36 @@
311354
"#| export\n",
312355
"def redir_url(request, 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(request)\n",
358+
" scheme = 'http' if host.split(':')[0] in (\"localhost\", \"127.0.0.1\") else 'https'\n",
359+
" return f\"{scheme}://{host}{redir_path}\""
360+
]
361+
},
362+
{
363+
"cell_type": "code",
364+
"execution_count": null,
365+
"id": "fee2db6c",
366+
"metadata": {},
367+
"outputs": [
368+
{
369+
"name": "stdout",
370+
"output_type": "stream",
371+
"text": [
372+
"Localhost: http://localhost:8000/redirect\n",
373+
"With X-Forwarded-Host: https://example.com/redirect\n",
374+
"Production: https://myapp.com/redirect\n"
375+
]
376+
}
377+
],
378+
"source": [
379+
"from types import SimpleNamespace\n",
380+
"from urllib.parse import urlparse\n",
381+
"\n",
382+
"mock_request_prod = SimpleNamespace(headers={}, url=SimpleNamespace(netloc='myapp.com', hostname='myapp.com'))\n",
383+
"\n",
384+
"print(\"Localhost:\", redir_url(mock_request_localhost, '/redirect'))\n",
385+
"print(\"With X-Forwarded-Host:\", redir_url(mock_request_with_forward, '/redirect'))\n",
386+
"print(\"Production:\", redir_url(mock_request_prod, '/redirect'))"
316387
]
317388
},
318389
{
@@ -447,8 +518,8 @@
447518
"source": [
448519
"#| export\n",
449520
"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)"
521+
"def url_match(request, patterns=http_patterns):\n",
522+
" return any(re.match(pattern, get_host(request).split(':')[0]) for pattern in patterns)"
452523
]
453524
},
454525
{
@@ -475,7 +546,7 @@
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",
477548
" 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",
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",

0 commit comments

Comments
 (0)