☰
Livebook 自定义 JavaScript 沙箱机制解析:iframe 隔离服务、消息协议与部署实践
2026/10/12 6:48:18 网站建设 项目流程
  • 开发工具
  • 数据科学

【免费下载链接】livebook

Automate code & data workflows with interactive Elixir notebooks

项目地址:https://gitcode.com/gh_mirrors/li/livebook
点击查看免费下载

Livebook 允许用户在 notebook 中运行自定义 JavaScript(即 JS View 机制),用于构建绘图、地图等交互式输出类型。由于脚本内容由用户编写、来源不受信任,Livebook 将其严格限制在 iframe 沙箱中执行:运行在 http 协议时由本机独立端口提供页面,运行在 https 协议时则由托管在 livebookusercontent.com 目录对应的livebook_space项目)安全提供。阅读本文,你将完整掌握该 iframe 沙箱的架构设计、跨源消息协议、主应用与托管应用的配置要点,以及独立应用的构建与部署方式。

为什么 Livebook 要在 iframe 中执行自定义 JavaScript

Livebook 的 JavaScript View 是"用自定义能力扩展 Livebook"的抽象层,也是定义交互式输出类型(如绘图、地图)的主要构建模块。其前端 Hook js_view.js 的注释明确说明:

JavaScript is defined by the user, so we sandbox the script execution inside an iframe.

也就是说,用户定义的脚本是不可信代码,必须与 Livebook 主页面进行隔离。iframe 是浏览器提供的原生隔离边界,配合sandbox属性与跨源加载,可以限制脚本对父页面 DOM、Cookie、存储等的访问能力。

在渲染层面,js_view_component.ex 通过phx-hook="JSView"挂载该 Hook,并向浏览器端传入ref、assets-base-path、js-path、session-token、connect-token、iframe-port、iframe-url等关键属性,作为后续建立 iframe 通信与资源加载的依据。

双通道服务模型:http 本机端口与 https 托管服务

iframe 页面的加载来源由浏览器当前协议决定,核心逻辑位于 js_view/iframe.js:

function getIframeUrl(iframePort, iframeUrl) { const protocol = window.location.protocol; if (iframeUrl) { return iframeUrl.replace(/^https?:/, protocol); } return protocol === "https:" ? "https://livebookusercontent.com/iframe/v5.html" : `http://${window.location.hostname}:${iframePort}/iframe/v5.html`; }
  • http 模式:Livebook 主应用在本机启动一个独立端口的静态服务(iframe_port),以http://<hostname>:<port>/iframe/v5.html提供页面;
  • https 模式:页面从https://livebookusercontent.com/iframe/v5.html加载,该域名正是运行本仓库 iframe 目录中livebook_space应用的服务;
  • 自定义覆盖:若配置了iframe-url(对应LIVEBOOK_IFRAME_URL),则以其为基础,仅将协议部分替换为当前页面协议,保持 http/https 一致性。

为什么 http 模式不能直接使用外部托管

iframe.js 的文件头注释给出了详细的设计考量:

  1. srcdoc与data:URL 不可用:这两种方式会禁用 Cookie 及摄像头、麦克风等浏览器 API,无法满足交互式输出的需求;
  2. 必须使用跨源加载:iframe 需要allow-scripts,若同时来自同一源并使用allow-same-origin,脚本就能直接操纵父页面,存在严重安全风险;
  3. 协议必须一致:https 页面中加载 http 源 iframe 会被浏览器拦截(混合内容),因此 https 模式必须使用另一个 https 源(livebookusercontent.com);
  4. 安全上下文限制:外部 http 内容不属于安全上下文,无法访问用户媒体设备。因此 http 模式不采用http://livebookusercontent.com,而是使用与 Livebook 主应用不同端口的本地端点——不同的端口意味着不同的源,既满足跨源隔离,又保留本地可访问性。

内容完整性校验

由于 iframe 页面承载了与父页面通信的协议代码,Livebook 会在加载前手动校验其 SHA-256 摘要,防止中间人篡改。校验常量与逻辑见 iframe.js:

const IFRAME_SHA256 = "wcqj5QWCo66osdAWDnEgPRFyL7nfe8oNqNggnw4vvW8="; function verifyIframeSource(iframeUrl) { if (!iframeVerificationPromise) { iframeVerificationPromise = fetch(iframeUrl) .then((response) => response.text()) .then((html) => { if (sha256Base64(html) !== IFRAME_SHA256) { throw new Error( `The iframe loaded from ${iframeUrl} doesn't have the expected checksum ${IFRAME_SHA256}` ); } }); } return iframeVerificationPromise; }

校验通过后,iframe 才被赋予沙箱属性与权限列表并设置src:

iframe.sandbox = "allow-scripts allow-same-origin allow-downloads allow-forms allow-modals allow-popups allow-top-navigation"; iframe.allow = "accelerometer; ambient-light-sensor; camera; display-capture; encrypted-media; fullscreen; geolocation; gyroscope; microphone; midi; usb; xr-spatial-tracking; clipboard-read; clipboard-write; bluetooth; serial; local-network-access";

allow权限列表按需开放了摄像头、麦克风、全屏、地理位置等能力,这正是交互式输出(如音频输入、图像输出)所依赖的。

iframe 静态服务应用(livebook_space)剖析

iframe 目录是一个独立的 Mix 项目(应用名livebook_space),仅用于在 https 场景下托管 iframe 静态页面。其依赖极简,见 mix.exs:唯一的运行时依赖是bandit ~> 1.0。

应用启动:Bandit 监听 4000 端口

application.ex 中通过Bandit启动 HTTP 服务器:

children = [ {Bandit, scheme: :http, plug: LivebookSpaceWeb.Plug, port: 4000} ]

路由与响应头:Plug 管道

plug.ex 使用Plug.Builder定义了完整处理链:

plug Plug.Static, from: {:livebook_space, "priv/static/iframe"}, at: "/iframe", cache_control_for_etags: "public, max-age=31536000", headers: [ {"access-control-allow-origin", "*"}, {"content-type", "text/html; charset=utf-8"} ] plug Plug.Static, from: :livebook_space, at: "/"

关键点:

  • 静态资源从priv/static/iframe目录读取,挂载在/iframe路径下;
  • 缓存策略max-age=31536000(一年):注释说明"iframes are versioned, so we cache them for long"——iframe 页面按版本号(v1~v5)组织,URL 变更即代表内容变更,因此可以放心长期缓存;
  • CORS 头access-control-allow-origin: *:允许 Livebook 主页面跨源 fetch 该页面内容并校验其完整性;
  • 显式指定content-type: text/html; charset=utf-8;
  • 根路径请求回退到index.html,其余未匹配请求返回 404。

版本化的页面文件位于 iframe/priv/static/iframe 目录下(v1.html至v5.html),当前 Livebook 前端加载的是v5.html(见上述getIframeUrl)。

主应用侧的 iframe 端点与配置

当 Livebook 运行在 http 模式时,iframe 页面由主应用自身提供。iframe_endpoint.ex 与livebook_space的 Plug 配置几乎一致:同样挂载/iframe、启用gzip: true、一年缓存与 CORS 头,静态目录为priv_path()/static/iframe。

该端点的启动由 application.ex 的iframe_server_specs/0负责:在 Web 端点启用(server?)时,用 Bandit 在iframe_port端口启动一个独立的LivebookWeb.IframeEndpoint服务,并继承主 HTTP 端点的:ip配置。若端口被占用(如桌面应用场景),则自动回退为随机端口,见 iframe_endpoint_start。

各环境的默认端口

iframe_port在不同环境下的默认值(见 config/dev.exs、config/prod.exs、config/test.exs):

环境默认 iframe 端口
dev4001
prod8081
test4003

该值的读取逻辑位于 config.ex(iframe_port/0),并支持通过环境变量覆盖:LIVEBOOK_IFRAME_PORT与LIVEBOOK_IFRAME_URL的解析见 livebook.ex 与 config.ex。

相关环境变量一览

环境变量作用
LIVEBOOK_IFRAME_PORT覆盖 iframe 服务端口(http 模式下使用)
LIVEBOOK_IFRAME_URL覆盖 iframe 页面加载地址(https 模式下常用)
LIVEBOOK_WITHIN_IFRAME标记 Livebook 自身被嵌入到外部 iframe 中(true时启用SameSite=None; Secure会话 Cookie,见 endpoint.ex)

iframe 页面与父页面的消息协议(v5.html)

iframe/priv/static/iframe/v5.html 是 iframe 侧协议实现的完整载体,全部通过window.parent.postMessage与父页面通信,分为握手、事件、同步与工具四个部分。

握手与模块加载

  1. iframe 加载完成后立即发送{ type: "ready" }(见 v5.html);
  2. 父页面回复readyReply,携带三项数据:
    • token:本次通信的随机令牌;
    • baseUrl:资源基地址,父页面随后动态创建<base>元素,使 iframe 内相对路径正确解析;
    • jsPath:视图专属 JS 模块的相对路径,iframe 用绝对地址import(\${baseUrl}${jsPath}`)动态加载(因为` 的修改不影响已开始的 import 调用);
  3. 父页面随后发送init消息,iframe 在 import 完成后调用模块的init(ctx, data)函数;若模块未导出init,则抛出明确错误,并在 iframe 内渲染错误信息(v5.html)。

暴露给用户脚本的 ctx API

v5.html 构建的ctx对象(v5.html)是用户 JS 模块唯一可用的接口:

API说明
ctx.rootiframe 内的根 DOM 容器
ctx.handleEvent(event, callback)注册来自服务端的事件处理器,同一事件仅允许注册一次;待处理事件先入队,处理器就绪后按序消费
ctx.pushEvent(event, payload)向服务端推送事件(携带令牌,{ type: "event", event, payload })
ctx.importCSS(url)/ctx.importJS(url)动态加载外部 CSS 与 JS 资源
ctx.handleSync(callback)注册同步回调,用于服务端发起 ping 前让 iframe 先把延迟的 UI 变更推送到服务端
ctx.selectSecret(callback, preselectName, options)请求父页面让用户选择 Secret,选中后通过secretSelected消息回调
ctx.setSmartCellEditorIntellisenseNode(node, cookie)为智能单元格编辑器设置智能感知节点

高度自适应与 DOM 事件转发

  • 高度同步:onReady阶段用ResizeObserver监听document.body,任何高度变化都以{ type: "resize", height }上报父页面,父页面据此调整占位元素与 iframe 尺寸(v5.html);
  • 事件转发:iframe 将mousedown、focus、keydown等 DOM 事件序列化后转发,并标注isTargetEditable(目标是否为input、textarea或contenteditable元素),父页面据此决定事件落点(v5.html)。

消息方向汇总

消息类型方向作用
readyiframe → 父告知页面就绪
readyReply父 → iframe下发 token、baseUrl、jsPath
init父 → iframe传入初始化数据并触发模块init
event双向用户事件推送 / 服务端事件下发
resizeiframe → 父高度变更上报
domEventiframe → 父DOM 事件转发
sync/syncReply父 ↔ iframe同步握手
selectSecret/secretSelected父 ↔ iframeSecret 选择流程

父页面 JSView Hook:加载、校验与通信

主应用侧的核心实现在 js_view.js,其职责包括:

  • 懒加载:通过waitUntilInViewport观察占位元素,只有当输出进入视口时才真正加载 iframe(js_view.js);
  • iframe 位置同步:createIframe把 iframe 提升到 notebook 根元素附近(避免单元格重排导致 iframe 反复重载),再用ResizeObserver与占位元素做绝对定位对齐(js_view.js);
  • 令牌鉴权:父页面生成childToken,iframe 发来的每条消息都必须携带匹配令牌,否则抛出Token mismatch;同时代码注释也诚实地指出这是尽力而为的防护——脚本若在极端情况下拿到令牌仍可能发送消息,但其中最"危险"的动作仅限于快捷键转发(js_view.js);
  • 服务端通道:通过 Phoenix Channeljs_view与 Livebook 会话通信,令牌经 js_view_component.ex 以Phoenix.Token.sign/3签发;init:<ref>、event:<ref>、error:<ref>、pong:<ref>等消息的编解码见 channel.js,其中二进制负载使用带注解的 ArrayBuffer 编码以支持高效传输;
  • 资产 CDN 回退:getAssetsBaseUrl先探测 Livebook 公共端点/public/health是否可无认证访问,若不可访问且存在assets-cdn-url,则回退到 CDN 地址,解决认证代理环境下 iframe 内跨源加载资源的问题(js_view.js)。

安全设计要点汇总

综合上述实现,Livebook 的 iframe 沙箱构成四层防护:

  1. 跨源隔离:http 模式使用不同端口、https 模式使用不同域名的独立源,规避allow-same-origin+allow-scripts同源组合的风险;
  2. 内容完整性:加载前对v5.html做 SHA-256 校验,防止托管端内容被篡改;
  3. 通信令牌:每次会话生成随机childToken,父页面校验所有子消息来源;
  4. 最小权限:sandbox属性限定脚本行为,allow属性按需开放硬件与系统权限;配合一年期缓存与全开放 CORS 头,在性能(版本化 URL 长期缓存)与安全(校验+跨源)之间取得平衡。

构建与部署 livebook_space

独立 iframe 托管应用同样提供完整的生产化部署方案:

  • Docker 镜像:Dockerfile 采用两阶段构建:第一阶段基于hexpm/elixir:1.13.2-erlang-24.1.7-alpine-3.15.0编译并产出 release(要求 Elixir ~> 1.13,与 mix.exs 一致),第二阶段基于alpine:3.15.0仅保留运行产物与openssl、ncurses-libs、libstdc++运行时依赖,最终以/app/bin/livebook_space start启动;
  • Fly.io 部署:fly.toml 定义应用livebook-space,内部端口 4000,对外暴露 80(http)与 443(tls+http),并配置 TCP 健康检查(grace_period = "30s"、interval = "15s"、restart_limit = 6、timeout = "2s")。

本地调试时,可进入iframe目录执行mix deps.get && mix run --no-halt启动该服务,再配合 Livebook 主应用的环境变量(如 https 模式下设置LIVEBOOK_IFRAME_URL)进行联调。

小结

Livebook 的 iframe 沙箱并非简单的<iframe>拼装,而是一套精心设计的跨源托管体系:独立端口/独立域名保证源隔离,版本化静态页面配合一年缓存与 SHA-256 校验保证内容安全与可验证,ready/readyReply握手配合随机令牌建立可信的双向 postMessage 通道,最终以init为界把用户自定义 JS 模块安全地接入选定输出区域。理解 iframe 目录所代表的livebook_space服务与主应用侧 js_view.js、iframe_endpoint.ex 的协作关系,是深入定制 Livebook 交互式输出与排查 iframe 加载问题的关键。

  • 开发工具
  • 数据科学

【免费下载链接】livebook

Automate code & data workflows with interactive Elixir notebooks

项目地址:https://gitcode.com/gh_mirrors/li/livebook
点击查看免费下载
上一篇:手把手玩转QQ音乐解析:复制一段Cookie,几分钟把想听的歌全存到本地
下一篇:服务器重启后你的 SearXNG 还在吗?systemd 开机自启动配置实战

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询