Skip to content

Commit

Permalink
Fix rendering on JupyterHub 4.1: respect absolute_url for protocol …
Browse files Browse the repository at this point in the history
…determination, support `data-absolute-url` (bokeh#14003)

* Fix rendering on JupyterHub 4.1

* Get rid of the mutable variable

Co-authored-by: Mateusz Paprocki <[email protected]>

* Add an example

* Do not use type casting, define a custom type guard

* Reduce size of the negative example iframe

* Lint

* Move to `app/iframe_embed`

* Skip `iframe_embed` for now

* Retrigger CI

* Move to `examples/server/api`

---------

Co-authored-by: Mateusz Paprocki <[email protected]>
  • Loading branch information
krassowski and mattpap authored Aug 5, 2024
1 parent 3a74368 commit 0300cc4
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 4 deletions.
23 changes: 20 additions & 3 deletions bokehjs/src/lib/embed/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import type {EmbedTarget} from "./dom"

// @internal
export function _get_ws_url(app_path: string | undefined, absolute_url: string | undefined): string {
let protocol = "ws:"
if (window.location.protocol == "https:") {
protocol = "wss:"
// if in an `srcdoc` iframe, try to get the absolute URL
// from the `data-absolute-url` attribute if not passed explicitly
if (absolute_url === undefined && _is_frame_HTMLElement(frameElement) && frameElement.dataset.absoluteUrl !== undefined) {
absolute_url = frameElement.dataset.absoluteUrl
}

let loc: HTMLAnchorElement | Location
Expand All @@ -21,6 +22,7 @@ export function _get_ws_url(app_path: string | undefined, absolute_url: string |
loc = window.location
}

const protocol = loc.protocol == "https:" ? "wss:" : "ws:"
if (app_path != null) {
if (app_path == "/") {
app_path = ""
Expand All @@ -32,6 +34,21 @@ export function _get_ws_url(app_path: string | undefined, absolute_url: string |
return `${protocol}//${loc.host}${app_path}/ws`
}

function _is_frame_HTMLElement(frame: Element | null): frame is HTMLIFrameElement {
// `frameElement` is a delicate construct; it allows the document inside the frame to access
// some (but not all) properties of the parent element in which the frame document is embedded.
// Because it lives in a different DOM context than the frame's `window`, we cannot just use
// `frameElement instanceof HTMLIFrameElement`; we could use `window.parent.HTMLIFrameElement`
// but this can be blocked by CORS policy and throw an exception.
if (frame === null) {
return false
}
if (frame.tagName.toUpperCase() === "IFRAME") {
return true
}
return false
}

type WebSocketURL = string
type SessionID = string

Expand Down
9 changes: 9 additions & 0 deletions examples/server/api/iframe_embed/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This example demonstrates embedding in an iframe using srcdoc under restrictive CSP.

To view the example, run:

python main.py

in this directory, and navigate to:

http://localhost:5000
14 changes: 14 additions & 0 deletions examples/server/api/iframe_embed/bokeh_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import numpy as np

from bokeh.io import curdoc
from bokeh.plotting import figure

N = 4000
x = np.random.random(size=N) * 100
y = np.random.random(size=N) * 100
radii = np.random.random(size=N) * 1.5

p = figure(tools="", toolbar_location=None)
p.circle(x, y, radius=radii, fill_alpha=0.6)

curdoc().add_root(p)
67 changes: 67 additions & 0 deletions examples/server/api/iframe_embed/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'''This example demonstrates embedding in an iframe using srcdoc under restrictive CSP.
To view the example, run:
python main.py
in this directory, and navigate to:
http://localhost:5000
'''
import atexit
import subprocess

from flask import Flask, render_template_string

from bokeh.client import pull_session
from bokeh.embed.server import server_html_page_for_session
from bokeh.resources import INLINE

app_html = """
<!DOCTYPE html>
<html lang="en">
<body>
<p>
This is an example of cross-origin embedding using an iframe under restrictive Content Security Policy (CSP).
Under strict CSP iframe embedding with `src` does not work:
</p>
<iframe src="{{ app_url }}" width=100% height=50px></iframe>
<p>But it is still possible to embed with `srcdoc` attribute and using `data-absolute-url`:</p>
<iframe id="myiframe" width=100% height=500px></iframe>
<script>
const iframe = document.querySelector("#myiframe");
iframe.dataset.absoluteUrl = {{ app_url|tojson }};
iframe.srcdoc = {{ code|tojson }};
</script>
</body>
</html>
"""

app = Flask(__name__)

bokeh_process = subprocess.Popen(
['python', '-m', 'bokeh', 'serve', '--port=5151', '--allow-websocket-origin=localhost:5000', '--allow-websocket-origin=127.0.0.1:5000', 'bokeh_server.py'],
stdout=subprocess.PIPE,
)

@atexit.register
def kill_server():
bokeh_process.kill()


@app.after_request
def add_security_headers(resp):
resp.headers["Content-Security-Policy"] = "frame-ancestors 'none'"
return resp

@app.route('/')
def home():
app_url = "http://localhost:5151/bokeh_server"
with pull_session(url=app_url) as session:
code = server_html_page_for_session(session=session, resources=INLINE, title='test')
return render_template_string(app_html, code=code, app_url=app_url)


if __name__ == '__main__':
app.run()
2 changes: 1 addition & 1 deletion tests/examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
- path: "examples/output/webgl/*"

- path: "examples/output/apis/*"
skip: ["autoload_static.py", "autoload_static_flask.py", "components.py", "components_themed.py", "json_item.py", "json_item_themed.py", "server_document", "server_session"]
skip: ["autoload_static.py", "autoload_static_flask.py", "components.py", "components_themed.py", "iframe_embed", "json_item.py", "json_item_themed.py", "server_document", "server_session"]

- path: "examples/plotting/*"

Expand Down

0 comments on commit 0300cc4

Please sign in to comment.