-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add spec pages, catalog, and interactive views #3167
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
Changes from all commits
ba4b98d
0024ae6
2fe2172
5a6c327
94d8310
424dc35
ddc7f73
d959b87
974ed57
6290f89
a7e5775
1c46128
3a218e2
28d15f8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,151 @@ | ||||||
| """HTML proxy endpoint for interactive plots with size reporting.""" | ||||||
|
|
||||||
| from urllib.parse import urlparse | ||||||
|
|
||||||
| import httpx | ||||||
| from fastapi import APIRouter, HTTPException | ||||||
| from fastapi.responses import HTMLResponse | ||||||
|
|
||||||
|
|
||||||
| router = APIRouter(tags=["proxy"]) | ||||||
|
|
||||||
| # Script injected to report content size to parent window | ||||||
| # Uses specific origin (pyplots.ai) for postMessage security | ||||||
| SIZE_REPORTER_SCRIPT = """ | ||||||
| <script> | ||||||
| (function() { | ||||||
| function reportSize() { | ||||||
| try { | ||||||
| // Find the main content element (try common patterns for different libraries) | ||||||
| var content = document.querySelector( | ||||||
| '.bk-root, .vega-embed, .plotly, .chart-container, #container, .lp-plot, svg, canvas' | ||||||
| ) || document.body.firstElementChild || document.body; | ||||||
|
|
||||||
| // Get actual rendered size | ||||||
| var rect = content.getBoundingClientRect(); | ||||||
| var width = Math.max(rect.width, content.scrollWidth || 0, document.body.scrollWidth || 0); | ||||||
| var height = Math.max(rect.height, content.scrollHeight || 0, document.body.scrollHeight || 0); | ||||||
|
|
||||||
| // Add padding to account for action buttons, toolbars, and other UI elements | ||||||
| var padding = 40; | ||||||
| width += padding; | ||||||
| height += padding; | ||||||
|
|
||||||
| // Send to parent with specific origin for security | ||||||
| if (width > 0 && height > 0 && window.parent !== window) { | ||||||
| window.parent.postMessage({ | ||||||
| type: 'pyplots-size', | ||||||
| width: Math.ceil(width), | ||||||
| height: Math.ceil(height) | ||||||
| }, 'https://pyplots.ai'); | ||||||
| } | ||||||
| } catch (e) { | ||||||
| // Silently fail if postMessage is blocked | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Report after load and after delays (for async rendering libraries) | ||||||
| if (document.readyState === 'complete') { | ||||||
| setTimeout(reportSize, 100); | ||||||
| setTimeout(reportSize, 500); | ||||||
| setTimeout(reportSize, 1000); | ||||||
| } else { | ||||||
| window.addEventListener('load', function() { | ||||||
| setTimeout(reportSize, 100); | ||||||
| setTimeout(reportSize, 500); | ||||||
| setTimeout(reportSize, 1000); | ||||||
| }); | ||||||
| } | ||||||
| })(); | ||||||
| </script> | ||||||
| """ | ||||||
|
|
||||||
| # Allowed GCS bucket for security | ||||||
| ALLOWED_HOST = "storage.googleapis.com" | ||||||
| ALLOWED_BUCKET = "pyplots-images" | ||||||
|
|
||||||
|
|
||||||
| def build_safe_gcs_url(url: str) -> str | None: | ||||||
| """ | ||||||
| Validate URL and return a reconstructed safe GCS URL. | ||||||
|
|
||||||
| This prevents SSRF by constructing the URL from hardcoded values | ||||||
| instead of passing user input directly. | ||||||
|
|
||||||
| Args: | ||||||
| url: User-provided URL to validate | ||||||
|
|
||||||
| Returns: | ||||||
| Reconstructed safe URL or None if validation fails | ||||||
| """ | ||||||
| try: | ||||||
| parsed = urlparse(url) | ||||||
| # Must be HTTPS | ||||||
| if parsed.scheme != "https": | ||||||
| return None | ||||||
| # Must be exact host (no subdomains) | ||||||
| if parsed.netloc != ALLOWED_HOST: | ||||||
| return None | ||||||
| # Path must start with bucket name | ||||||
| path_parts = parsed.path.strip("/").split("/") | ||||||
| if len(path_parts) < 2: | ||||||
| return None | ||||||
| if path_parts[0] != ALLOWED_BUCKET: | ||||||
| return None | ||||||
| # Check for path traversal attempts | ||||||
| if ".." in parsed.path: | ||||||
| return None | ||||||
| # Validate path contains only safe characters (alphanumeric, hyphens, underscores, dots, slashes) | ||||||
|
||||||
| # Validate path contains only safe characters (alphanumeric, hyphens, underscores, dots, slashes) | |
| # Validate path contains only safe characters (alphanumeric, hyphens, underscores, dots, slashes, plus) |
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.
The postMessage origin is hardcoded to 'https://pyplots.ai' which will fail in development and staging environments. The InteractivePage allows the development origin (window.location.origin) when receiving messages, but the proxy script only sends to the production domain.
This mismatch means the size reporting won't work in local development. Consider using a dynamic origin based on the request's Referer header or passing the origin as a parameter to make the feature work in all environments.