-
Notifications
You must be signed in to change notification settings - Fork 16
Add Ghost Viewer #93
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
guerler
wants to merge
59
commits into
galaxyproject:main
Choose a base branch
from
guerler:ghost
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add Ghost Viewer #93
Changes from all commits
Commits
Show all changes
59 commits
Select commit
Hold shift + click to select a range
09f051c
Test service worker
guerler 914268c
Add prettier
guerler 87ff8a1
Allow base path
guerler fd35775
Reject external requests
guerler 5a168a5
Run prettier
guerler 5fb5a78
Adjust logs
guerler 60d1c10
Update
guerler e377f7d
Use insert
guerler 3a3c31b
Add scope
guerler b66adb8
Fix scope
guerler e5fced4
Add examples
guerler 7150ac4
Use stored index.html
guerler acc666e
Script path separation
guerler 153b473
Add wait routine
guerler 24de154
With injection test
guerler 8b0babb
Attempt to autodetect base path
guerler 76f3062
Pick parent level index.htm if multiple entries are available
guerler a73e4ff
Use base path locally
guerler 5748c85
Add incoming dictionary handling
guerler 815fede
Handle pathname
guerler 90819df
Fallback for local run
guerler d9c5f15
Fix scope handling
guerler d7120b1
Simplify debug scope
guerler 39523c5
Add xml
guerler 178d844
Generalize sw configuration options
guerler eaa1216
Block all request to Galaxy service except GET requests within scope
guerler 2ca083f
Add help text, test data and comments
guerler 26353e4
Simplify error message
guerler 6b704f5
Add main css styles
guerler 5b143e5
Properly clear file system
guerler 2e1fb56
Fix comment for consistency
guerler b809a5b
Clear cache when loading new static page
guerler a41d8c5
Update xml, scope content
guerler 2a8862d
Update test labels
guerler 3a84068
Fix description
guerler 3669963
Update description, and logo
guerler 9402ffd
Rename service script for clarity
guerler 4e9bf18
Replace logo
guerler 5c2ab03
Adjustments
guerler 76b7634
Avoid race condition
guerler 5f59ce7
Remove empty line
guerler dbe3b31
Remove empty line
guerler b9aedd4
Add error log, properly indent conditions
guerler 6d2a0c8
Add cleanup handler
guerler 4186dd3
Add cleanup handler
guerler 48b6a66
Load data just once
guerler 3f897a6
Use consistent naming
guerler 4fcdfc2
Add project
guerler 1f42bb8
Make package public
guerler f69c315
Update package version
guerler 51bc833
Clear filesystem after unregistration
guerler 38c82aa
Reload data upon page reload
guerler 82373c4
Simplify logo
guerler 7b0f74c
Fix qza only
guerler 71b48b8
Revise log
guerler 966e52c
Update package version
guerler 5cac88c
Fix margin
guerler fe63ae5
Make visualization embeddable
guerler cdc01f0
Store test dataset as constant
guerler File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Logs | ||
| logs | ||
| *.log | ||
| npm-debug.log* | ||
| yarn-debug.log* | ||
| yarn-error.log* | ||
| pnpm-debug.log* | ||
| lerna-debug.log* | ||
|
|
||
| node_modules | ||
| dist | ||
| dist-ssr | ||
| *.local | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
| !.vscode/extensions.json | ||
| .idea | ||
| .DS_Store | ||
| *.suo | ||
| *.ntvs* | ||
| *.njsproj | ||
| *.sln | ||
| *.sw? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| <!doctype html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <link rel="icon" type="image/svg+xml" href="/vite.svg" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Vite App</title> | ||
| </head> | ||
| <body> | ||
| <div id="app"></div> | ||
| <script type="module" src="/src/main.js"></script> | ||
| </body> | ||
| </html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| { | ||
| "name": "@galaxyproject/ghost", | ||
| "private": false, | ||
| "version": "0.0.4", | ||
| "type": "module", | ||
| "files": [ | ||
| "static" | ||
| ], | ||
| "scripts": { | ||
| "dev": "vite", | ||
| "build": "vite build", | ||
| "preview": "vite preview", | ||
| "prettier": "prettier --write 'package.json' '*.js' 'src/*.js' 'public/*.js'" | ||
| }, | ||
| "devDependencies": { | ||
| "jszip": "^3.10.1", | ||
| "prettier": "^3.6.2", | ||
| "vite": "^7.0.4" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| export default { | ||
| tabWidth: 4, | ||
| printWidth: 120, | ||
| bracketSameLine: true, | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| const DESTROY = 5000; | ||
| const TIMEOUT = 100; | ||
|
|
||
| const virtualFS = new Map(); | ||
|
|
||
| let ready = false; | ||
| let scope = ""; | ||
|
|
||
| const getMimeType = (path) => { | ||
| const mimeMap = { | ||
| ".css": "text/css", | ||
| ".js": "application/javascript", | ||
| ".json": "application/json", | ||
| ".jsonp": "application/javascript", | ||
| ".png": "image/png", | ||
| ".jpg": "image/jpeg", | ||
| ".html": "text/html", | ||
| ".svg": "image/svg+xml", | ||
| ".woff": "font/woff", | ||
| ".woff2": "font/woff2", | ||
| ".ttf": "font/ttf", | ||
| }; | ||
| const ext = path.slice(path.lastIndexOf(".")).split("?")[0]; | ||
| return mimeMap[ext] || "text/plain"; | ||
| }; | ||
|
|
||
| self.addEventListener("install", (event) => { | ||
| console.log("[GHOST] Installing..."); | ||
| event.waitUntil(self.skipWaiting()); | ||
| }); | ||
|
|
||
| self.addEventListener("activate", (event) => { | ||
| console.log("[GHOST] Activating..."); | ||
| event.waitUntil(clients.claim()); | ||
| }); | ||
|
|
||
| self.addEventListener("message", (event) => { | ||
| if (event.data.type === "CREATE") { | ||
| scope = event.data.scope; | ||
| const files = Object.entries(event.data.files); | ||
| for (const [path, content] of files) { | ||
| virtualFS.set(path, content); | ||
| } | ||
| console.log(`[GHOST] Serving ${files.length} files from ${scope}`); | ||
| ready = true; | ||
| } | ||
| if (event.data.type === "DESTROY") { | ||
| virtualFS.clear(); | ||
| console.log("[GHOST] Destroyed filesystem"); | ||
| ready = false; | ||
| } | ||
| }); | ||
|
|
||
| self.addEventListener("fetch", (event) => { | ||
| console.log("[GHOST] Intercepting..."); | ||
| const url = new URL(event.request.url); | ||
| const isSameOrigin = url.origin === self.location.origin; | ||
| const scoped = scope.endsWith("/") ? scope : scope + "/"; | ||
|
|
||
| // Only handle same-origin requests | ||
| if (isSameOrigin) { | ||
| if (event.request.method === "GET" && url.pathname.startsWith(scoped)) { | ||
| event.respondWith( | ||
| (async () => { | ||
| const start = Date.now(); | ||
| while (!ready) { | ||
| if (Date.now() - start > TIMEOUT * TIMEOUT) { | ||
| return new Response("Filesystem not ready", { | ||
| status: 503, | ||
| headers: { "Cache-Control": "no-store" }, | ||
| }); | ||
| } | ||
| await new Promise((r) => setTimeout(r, TIMEOUT)); | ||
| } | ||
| const path = decodeURIComponent(url.pathname).split("?")[0]; | ||
| if (virtualFS.has(path)) { | ||
| const mime = getMimeType(path); | ||
| const content = virtualFS.get(path); | ||
| return new Response(content, { headers: { "Content-Type": mime } }); | ||
| } | ||
| return new Response("Not Found", { status: 404, statusText: "Not Found" }); | ||
| })(), | ||
| ); | ||
| } else { | ||
| // Block other same-origin requests (outside our scope) | ||
| console.error("[GHOST] Blocking same-origin request:", url.href); | ||
| event.respondWith( | ||
| new Response("Blocked by Service Worker", { | ||
| status: 403, | ||
| statusText: "Forbidden", | ||
| headers: { "Content-Type": "text/plain" }, | ||
| }), | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| // Function to check if there are any active clients within the same scope | ||
| const checkClientsAndCleanup = async () => { | ||
| // Get all clients that are within the same scope | ||
| const clientsList = await clients.matchAll({ | ||
| type: "window", | ||
| includeUncontrolled: true, | ||
| }); | ||
|
|
||
| // Filter clients by scope | ||
| const scopedClients = clientsList.filter((client) => { | ||
| const clientUrl = new URL(client.url); | ||
| return clientUrl.pathname.startsWith(scope); | ||
| }); | ||
|
|
||
| // If no scoped clients are present, start cleanup | ||
| if (scopedClients.length === 0) { | ||
| // Clear interval | ||
| clearInterval(clientCheckInterval); | ||
|
|
||
| // Cleanup logic: unregister service worker or any other resource cleanup | ||
| await self.registration.unregister(); | ||
|
|
||
| // Clear filesystem | ||
| virtualFS.clear(); | ||
| console.log("[GHOST] Service worker unregistered"); | ||
| } | ||
| }; | ||
|
|
||
| // Start checking for active clients | ||
| const clientCheckInterval = setInterval(checkClientsAndCleanup, DESTROY); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <!DOCTYPE visualization SYSTEM "../../visualization.dtd"> | ||
| <visualization name="GHost" embeddable="true"> | ||
| <description>Virtual Webserver for HTML archives</description> | ||
| <data_sources> | ||
| <data_source> | ||
| <model_class>HistoryDatasetAssociation</model_class> | ||
| <test test_attr="ext">qzv</test> | ||
| </data_source> | ||
| </data_sources> | ||
| <params> | ||
| <param required="true">dataset_id</param> | ||
| </params> | ||
| <entry_point entry_point_type="script" src="index.js" css="index.css" /> | ||
| <tests> | ||
| <test> | ||
| <param name="dataset_id" label="Raincloud Baseline" ftype="qzv" value="https://raw.githubusercontent.com/qiime2/q2-fmt/master/demo/raincloud-baseline0.qzv" /> | ||
| </test> | ||
| <test> | ||
| <param name="dataset_id" label="FastTree Empire" ftype="qzv" value="https://raw.githubusercontent.com/caporaso-lab/q2view-visualizations/main/uu-fasttree-empire.qzv" /> | ||
| </test> | ||
| </tests> | ||
| <help format="markdown"><![CDATA[ | ||
| # What is GHost? | ||
|
|
||
| GHost is a Galaxy visualization for securely displaying interactive websites that are packaged | ||
| as QIIME 2 visualization files (`.qzv`) or similar ZIP archives containing an `index.html` file. | ||
| These archives often contain JavaScript, and data assets for rich, client-side exploration of analysis results. | ||
|
|
||
| When you open a `.qzv` dataset with GHost: | ||
|
|
||
| 1. The file is fetched and unzipped in your browser. | ||
| 2. The `index.html` is located and assets load through a **service worker** at a dedicated scope. | ||
| 3. The service worker serves only the files from the archive, blocking all other same-origin requests | ||
| (for example, API calls) to prevent malicious content from interacting with Galaxy. | ||
|
|
||
| ## Key Features | ||
|
|
||
| - **Secure Sandbox**: Blocks access to Galaxy APIs from within the visualization. | ||
| - **Dynamic Asset Loading**: No need to pre-extract archives on the server. | ||
| - **Format Agnostic**: Works with any ZIP that contains a self-contained HTML visualization. | ||
|
|
||
| ## Security Notes | ||
|
|
||
| - Only GET requests within the visualization's virtual scope are served. | ||
| - All other same-origin requests are blocked with a `403 Forbidden` response. | ||
| - Cross-origin requests are unaffected but subject to normal browser CORS rules. | ||
|
|
||
| ## When to Use GHost | ||
|
|
||
| GHost is ideal for: | ||
| - Viewing `.qzv` files generated by QIIME 2. | ||
| - Displaying any self-contained HTML/JS/CSS visualization from a ZIP. | ||
| - Running third-party visualizations in a safe, sandboxed environment inside Galaxy. | ||
| ]]></help> | ||
| </visualization> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| :root { | ||
| --message-background: rgba(255, 255, 255, 0.9); | ||
| --message-color: rgba(0, 0, 0, 0.9); | ||
| } | ||
|
|
||
| #message { | ||
| background: var(--message-background); | ||
| border-radius: 4px; | ||
| color: var(--message-color); | ||
| display: none; | ||
| font-family: sans-serif; | ||
| font-weight: 100; | ||
| font-size: 0.85rem; | ||
| left: 1rem; | ||
| padding: 0.7rem; | ||
| position: absolute; | ||
| right: 1rem; | ||
| top: 1rem; | ||
| word-break: break-all; | ||
| z-index: 9999; | ||
| } | ||
|
|
||
| body { | ||
| margin: 0; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get requests include the session key, this might be a concern. How does the interception work, can a script that's loaded through the worker makes requests using their own shipped networking library ? Have you tried exploiting this with malicious archives ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am experimenting with an option that injects the actual dataset and sandboxes the entire visualization. This would limit the file size, which is a drawback, but likely make it safe fyi.