Skip to content

Commit

Permalink
Move session prefixes from handlers to Application (#7318)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr committed Sep 24, 2024
1 parent 81ccf03 commit 9f51bb4
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 83 deletions.
23 changes: 23 additions & 0 deletions panel/io/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import (
TYPE_CHECKING, Any, Callable, Mapping,
)
from urllib.parse import urljoin

import bokeh.command.util

Expand Down Expand Up @@ -121,8 +122,30 @@ def add(self, handler: Handler) -> None:
handler._on_session_destroyed = _on_session_destroyed
super().add(handler)

def _set_session_prefix(self, doc):
session_context = doc.session_context
if not (session_context and session_context.server_context):
return
request = session_context.request
app_context = session_context.server_context.application_context
prefix = request.uri.replace(app_context._url, '')
if not prefix.endswith('/'):
prefix += '/'
base_url = urljoin('/', prefix)
rel_path = '/'.join(['..'] * app_context._url.strip('/').count('/'))

# Handle autoload.js absolute paths
abs_url = request.arguments.get('bokeh-absolute-url')
if abs_url:
rel_path = abs_url[0].decode('utf-8').replace(app_context._url, '')

with set_curdoc(doc):
state.base_url = base_url
state.rel_path = rel_path

def initialize_document(self, doc):
logger.info(LOG_SESSION_LAUNCHING, id(doc))
self._set_session_prefix(doc)
super().initialize_document(doc)
if doc in state._templates and doc not in state._templates[doc]._documents:
template = state._templates[doc]
Expand Down
121 changes: 46 additions & 75 deletions panel/io/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,12 @@
import sys
import uuid

from contextlib import contextmanager
from functools import partial, wraps
from html import escape
from typing import (
TYPE_CHECKING, Any, Callable, Mapping, Optional,
)
from urllib.parse import urljoin, urlparse
from urllib.parse import urlparse

import bokeh
import param
Expand Down Expand Up @@ -56,7 +55,7 @@

# Internal imports
from ..config import config
from ..util import edit_readonly, fullpath
from ..util import fullpath
from ..util.warnings import warn
from .application import build_applications
from .document import ( # noqa
Expand Down Expand Up @@ -325,32 +324,6 @@ async def stop_autoreload():

bokeh.server.server.Server = Server

class SessionPrefixHandler:

@contextmanager
def _session_prefix(self):
prefix = self.request.uri.replace(self.application_context._url, '')
if not prefix.endswith('/'):
prefix += '/'
base_url = urljoin('/', prefix)
rel_path = '/'.join(['..'] * self.application_context._url.strip('/').count('/'))
old_url, old_rel = state.base_url, state.rel_path

# Handle autoload.js absolute paths
abs_url = self.get_argument('bokeh-absolute-url', default=None)
if abs_url is not None:
rel_path = abs_url.replace(self.application_context._url, '')

with edit_readonly(state):
state.base_url = base_url
state.rel_path = rel_path
try:
yield
finally:
with edit_readonly(state):
state.base_url = old_url
state.rel_path = old_rel

class LoginUrlMixin:
"""
Overrides the AuthRequestHandler.get_login_url implementation to
Expand All @@ -369,7 +342,7 @@ def get_login_url(self):
raise RuntimeError('login_url or get_login_url() must be supplied when authentication hooks are enabled')


class DocHandler(LoginUrlMixin, BkDocHandler, SessionPrefixHandler):
class DocHandler(LoginUrlMixin, BkDocHandler):

@authenticated
async def get_session(self):
Expand Down Expand Up @@ -488,50 +461,49 @@ async def get(self, *args, **kwargs):
return

app = self.application
with self._session_prefix():
key_func = state._session_key_funcs.get(self.request.path, lambda r: r.path)
old_request = key_func(self.request) in state._sessions
session = await self.get_session()
if old_request and state._sessions.get(key_func(self.request)) is session:
session_id = generate_session_id(
secret_key=self.application.secret_key,
signed=self.application.sign_sessions
)
payload = get_token_payload(session.token)
payload.update(payload)
del payload['session_expiry']
token = generate_jwt_token(
session_id,
secret_key=app.secret_key,
signed=app.sign_sessions,
expiration=app.session_token_expiration,
extra_payload=payload
key_func = state._session_key_funcs.get(self.request.path, lambda r: r.path)
old_request = key_func(self.request) in state._sessions
session = await self.get_session()
if old_request and state._sessions.get(key_func(self.request)) is session:
session_id = generate_session_id(
secret_key=self.application.secret_key,
signed=self.application.sign_sessions
)
payload = get_token_payload(session.token)
payload.update(payload)
del payload['session_expiry']
token = generate_jwt_token(
session_id,
secret_key=app.secret_key,
signed=app.sign_sessions,
expiration=app.session_token_expiration,
extra_payload=payload
)
else:
token = session.token
logger.info(LOG_SESSION_CREATED, id(session.document))
with set_curdoc(session.document):
resources = Resources.from_bokeh(self.application.resources())
# Session authorization callback
authorized, auth_error = self._authorize(session=True)
if authorized:
page = server_html_page_for_session(
session, resources=resources, title=session.document.title,
token=token, template=session.document.template,
template_variables=session.document.template_variables,
)
elif authorized is None:
return
else:
token = session.token
logger.info(LOG_SESSION_CREATED, id(session.document))
with set_curdoc(session.document):
resources = Resources.from_bokeh(self.application.resources())
# Session authorization callback
authorized, auth_error = self._authorize(session=True)
if authorized:
page = server_html_page_for_session(
session, resources=resources, title=session.document.title,
token=token, template=session.document.template,
template_variables=session.document.template_variables,
)
elif authorized is None:
return
else:
page = self._render_auth_error(auth_error)
page = self._render_auth_error(auth_error)

self.set_header("Content-Type", 'text/html')
self.write(page)

per_app_patterns[0] = (r'/?', DocHandler)

# Patch Bokeh Autoload handler
class AutoloadJsHandler(BkAutoloadJsHandler, SessionPrefixHandler):
class AutoloadJsHandler(BkAutoloadJsHandler):
''' Implements a custom Tornado handler for the autoload JS chunk
'''
Expand All @@ -550,16 +522,15 @@ async def get(self, *args, **kwargs) -> None:
else:
server_url = None

with self._session_prefix():
session = await self.get_session()
with set_curdoc(session.document):
resources = Resources.from_bokeh(
self.application.resources(server_url), absolute=True
)
js = autoload_js_script(
session.document, resources, session.token, element_id,
app_path, absolute_url, absolute=True
)
session = await self.get_session()
with set_curdoc(session.document):
resources = Resources.from_bokeh(
self.application.resources(server_url), absolute=True
)
js = autoload_js_script(
session.document, resources, session.token, element_id,
app_path, absolute_url, absolute=True
)

self.set_header("Content-Type", 'application/javascript')
self.write(js)
Expand Down
48 changes: 40 additions & 8 deletions panel/io/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,6 @@ class _state(param.Parameterized):
apps to indicate their state to a user.
"""

base_url = param.String(default='/', readonly=True, doc="""
Base URL for all server paths.""")

busy = param.Boolean(default=False, readonly=True, doc="""
Whether the application is currently busy processing a user
callback.""")
Expand All @@ -104,24 +101,27 @@ class _state(param.Parameterized):
Object with encrypt and decrypt methods to support encryption
of secret variables including OAuth information.""")

rel_path = param.String(default='', readonly=True, doc="""
Relative path from the current app being served to the root URL.
If application is embedded in a different server via autoload.js
this will instead reflect an absolute path.""")

session_info = param.Dict(default={'total': 0, 'live': 0,
'sessions': {}}, doc="""
Tracks information and statistics about user sessions.""")

webdriver = param.Parameter(default=None, doc="""
Selenium webdriver used to export bokeh models to pngs.""")

_base_url = param.String(default='/', readonly=True, doc="""
Base URL for all server paths.""")

_busy_counter = param.Integer(default=0, doc="""
Count of active callbacks current being processed.""")

_memoize_cache = param.Dict(default={}, doc="""
A dictionary used by the cache decorator.""")

_rel_path = param.String(default='', readonly=True, doc="""
Relative path from the current app being served to the root URL.
If application is embedded in a different server via autoload.js
this will instead reflect an absolute path.""")

# Holds temporary curdoc overrides per thread
_curdoc = ContextVar('curdoc', default=None)

Expand Down Expand Up @@ -220,6 +220,10 @@ class _state(param.Parameterized):
_oauth_user_overrides = {}
_active_users = Counter()

# Paths
_rel_paths: ClassVar[WeakKeyDictionary[Document, str]] = WeakKeyDictionary()
_base_urls: ClassVar[WeakKeyDictionary[Document, str]] = WeakKeyDictionary()

# Watchers
_watch_events: list[asyncio.Event] = []

Expand Down Expand Up @@ -1019,6 +1023,34 @@ def headers(self) -> dict[str, str | list[str]]:
"""
return self.curdoc.session_context.request.headers if self.curdoc and self.curdoc.session_context else {}

@property
def base_url(self) -> str:
base_url = self._base_url
if self.curdoc:
return self._base_urls.get(self.curdoc, base_url)
return base_url

@base_url.setter
def base_url(self, value: str):
if self.curdoc:
self._base_urls[self.curdoc] = value
else:
self._base_url = value

@property
def rel_path(self) -> str | None:
rel_path = self._rel_path
if self.curdoc:
return self._rel_paths.get(self.curdoc, rel_path)
return rel_path

@rel_path.setter
def rel_path(self, value: str | None):
if self.curdoc:
self._rel_paths[self.curdoc] = value
else:
self._rel_path = value

@property
def loaded(self) -> bool:
"""
Expand Down

0 comments on commit 9f51bb4

Please sign in to comment.