diff --git a/binderhub/base.py b/binderhub/base.py index b002bbd95..2d91ef0c9 100644 --- a/binderhub/base.py +++ b/binderhub/base.py @@ -137,11 +137,19 @@ def get_current_user(self): @property def template_namespace(self): - return dict( + + ns = dict( static_url=self.static_url, banner=self.settings["banner_message"], - **self.settings.get("template_variables", {}), + auth_enabled=self.settings["auth_enabled"], + ) + if self.settings["auth_enabled"]: + ns["api_token"] = self.hub_auth.get_token(self) or "" + + ns.update( + self.settings.get("template_variables", {}), ) + return ns def set_default_headers(self): headers = self.settings.get("headers", {}) diff --git a/binderhub/builder.py b/binderhub/builder.py index ca1deafbe..c97704726 100644 --- a/binderhub/builder.py +++ b/binderhub/builder.py @@ -247,6 +247,10 @@ def _get_build_only(self): return build_only + def redirect(self, *args, **kwargs): + # disable redirect to login, which won't work for EventSource + raise HTTPError(403) + @authenticated async def get(self, provider_prefix, _unescaped_spec): """Get a built image for a given spec and repo provider. diff --git a/binderhub/static/js/index.js b/binderhub/static/js/index.js index 536a4e278..d677e78ae 100644 --- a/binderhub/static/js/index.js +++ b/binderhub/static/js/index.js @@ -1,7 +1,6 @@ /* If this file gets over 200 lines of code long (not counting docs / comments), start using a framework */ import ClipboardJS from "clipboard"; -import "event-source-polyfill"; import { BinderRepository } from "@jupyterhub/binderhub-client"; import { updatePathText } from "./src/path"; @@ -61,11 +60,14 @@ async function build(providerSpec, log, fitAddon, path, pathType) { $(".on-build").removeClass("hidden"); const buildToken = $("#build-token").data("token"); + let apiToken = $("#api-token").data("token"); const buildEndpointUrl = new URL("build", BASE_URL); const image = new BinderRepository( providerSpec, buildEndpointUrl, buildToken, + false, + { apiToken }, ); for await (const data of image.fetch()) { diff --git a/binderhub/templates/index.html b/binderhub/templates/index.html index 6260c9c9d..be2235260 100644 --- a/binderhub/templates/index.html +++ b/binderhub/templates/index.html @@ -3,6 +3,7 @@ {% block head %} + {{ super() }} {% endblock head %} diff --git a/binderhub/templates/loading.html b/binderhub/templates/loading.html index dd6369ef2..16abb8199 100644 --- a/binderhub/templates/loading.html +++ b/binderhub/templates/loading.html @@ -14,6 +14,7 @@ + {{ super() }} diff --git a/js/packages/binderhub-client/lib/index.js b/js/packages/binderhub-client/lib/index.js index 9401eea7f..c9a622cc5 100644 --- a/js/packages/binderhub-client/lib/index.js +++ b/js/packages/binderhub-client/lib/index.js @@ -1,9 +1,21 @@ -import { NativeEventSource, EventSourcePolyfill } from "event-source-polyfill"; +import { fetchEventSource } from "@microsoft/fetch-event-source"; import { EventIterator } from "event-iterator"; -// Use native browser EventSource if available, and use the polyfill if not available -const EventSource = NativeEventSource || EventSourcePolyfill; - +function _getXSRFToken() { + // from @jupyterlab/services + let cookie = ""; + try { + cookie = document.cookie; + } catch (e) { + // e.g. SecurityError in case of CSP Sandbox + return null; + } + const xsrfTokenMatch = cookie.match("\\b_xsrf=([^;]*)\\b"); + if (xsrfTokenMatch) { + return xsrfTokenMatch[1]; + } + return null; +} /** * Build (and optionally launch) a repository by talking to a BinderHub API endpoint */ @@ -14,8 +26,15 @@ export class BinderRepository { * @param {URL} buildEndpointUrl API URL of the build endpoint to talk to * @param {string} [buildToken] Optional JWT based build token if this binderhub installation requires using build tokens * @param {boolean} [buildOnly] Opt out of launching built image by default by passing `build_only` param + * @param {string} [apiToken] Optional Bearer token for authenticating requests */ - constructor(providerSpec, buildEndpointUrl, buildToken, buildOnly) { + constructor( + providerSpec, + buildEndpointUrl, + buildToken, + buildOnly, + { apiToken }, + ) { this.providerSpec = providerSpec; // Make sure that buildEndpointUrl is a real URL - this ensures hostname is properly set if (!(buildEndpointUrl instanceof URL)) { @@ -40,6 +59,7 @@ export class BinderRepository { if (buildOnly) { this.buildUrl.searchParams.append("build_only", "true"); } + this.apiToken = apiToken; this.eventIteratorQueue = null; } @@ -67,26 +87,37 @@ export class BinderRepository { * @returns {AsyncIterable} An async iterator yielding responses from the API as they come in */ fetch() { - this.eventSource = new EventSource(this.buildUrl); - return new EventIterator((queue) => { + const headers = {}; + if (this.apiToken && this.apiToken.length > 0) { + headers["Authorization"] = `Bearer ${this.apiToken}`; + } else { + const xsrf = _getXSRFToken(); + if (xsrf) { + headers["X-Xsrftoken"] = xsrf; + } + } + return new EventIterator(async (queue) => { this.eventIteratorQueue = queue; - this.eventSource.onerror = () => { - queue.push({ - phase: "failed", - message: "Failed to connect to event stream\n", - }); - queue.stop(); - }; - - this.eventSource.addEventListener("message", (event) => { - // console.log("message received") - // console.log(event) - const data = JSON.parse(event.data); - // FIXME: fix case of phase/state upstream - if (data.phase) { - data.phase = data.phase.toLowerCase(); - } - queue.push(data); + await fetchEventSource(this.buildUrl, { + headers, + onerror: () => { + queue.push({ + phase: "failed", + message: "Failed to connect to event stream\n", + }); + queue.stop(); + }, + + onmessage: (event) => { + // console.log("message received") + // console.log(event) + const data = JSON.parse(event.data); + // FIXME: fix case of phase/state upstream + if (data.phase) { + data.phase = data.phase.toLowerCase(); + } + queue.push(data); + }, }); }); } diff --git a/js/packages/binderhub-client/package.json b/js/packages/binderhub-client/package.json index b2ab42c3c..db3a2ec7c 100644 --- a/js/packages/binderhub-client/package.json +++ b/js/packages/binderhub-client/package.json @@ -14,7 +14,7 @@ }, "homepage": "https://github.com/jupyterhub/binderhub#readme", "dependencies": { - "event-source-polyfill": "^1.0.31", + "@microsoft/fetch-event-source": "^2.0.1", "event-iterator": "^2.0.0" } } diff --git a/package.json b/package.json index e08b94bbc..9281c6407 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,6 @@ "dependencies": { "bootstrap": "^3.4.1", "clipboard": "^2.0.11", - "event-source-polyfill": "^1.0.31", "jquery": "^3.6.4", "xterm": "^5.1.0", "xterm-addon-fit": "^0.7.0"