跳转到内容
本站为社区维护的非官方中文翻译,与 Cloudflare, Inc. 无关联。本站不提供账户登录,请勿在任何非官方站点输入您的 Cloudflare 账户信息。官方文档:developers.cloudflare.com

迁移预览 URL

最后更新 查看 MarkdownAgent 设置

在 Sandbox SDK 0.12 中,exposePort() 在 Durable Object 存储中为每个端口存储令牌。proxyToSandbox() 将 8080-ada-<TOKEN>.example.com 这类主机名路由到该端口上的服务器。在 1.0 中,你的 Worker 路由这些主机名,Durable Object 检查令牌并用 getTcpPort() 转发请求。本页上的代码读取 0.12 存储令牌的位置,因此用户已有的预览 URL 会继续有效。

开始之前

你的 1.0 类已替换 0.12 Sandbox 类,并保留其名称及其 Sandbox 绑定。有关该类,请参阅 替换 Sandbox 类。

本页替换 exposePort()、unexposePort()、getExposedPorts()、isPortExposed()、validatePortToken()、proxyToSandbox() 和 wsConnect()。保留服务预览主机名的通配符 DNS 记录、证书和 Worker 路由。1.0 会原样使用它们。

本页不涵盖预览到达的服务器。要启动它,请参阅 迁移后台进程。

替换预览调用

  1. 将这些声明添加到 src/index.ts 的顶层:

    src/index.tsts
    type PortToken = { token: string; name?: string };
    type Stored = Record<string, string | PortToken>;
    
    // The key where 0.12 stored preview tokens.
    const PORT_TOKENS = "portTokens";
    
    function previewUrl(
        port: number,
        name: string,
        token: string,
        hostname: string,
    ): string {
        return `https://${port}-${name}-${token}.${hostname}/`;
    }
    
    // 16 characters from [a-z0-9_], as 0.12 generated them.
    function generateToken(): string {
        const bytes = crypto.getRandomValues(new Uint8Array(12));
        return btoa(String.fromCharCode(...bytes))
            .replace(/[+/]/g, "_")
            .toLowerCase();
    }
  2. 向 Durable Object 类添加读取令牌的方法以及 exposePort():

    src/index.tsts
    export class MySandbox extends DurableObject<Env> {
        // ...
    
        private async readPortTokens(): Promise<Record<string, PortToken>> {
            const stored = await this.ctx.storage.get<Stored>(PORT_TOKENS);
            const tokens: Record<string, PortToken> = {};
    
            // Early 0.12 versions stored the token as a string.
            for (const [port, value] of Object.entries(stored ?? {})) {
                tokens[port] =
                    typeof value === "string" ? { token: value } : value;
            }
    
            return tokens;
        }
    
        async exposePort(
            sandboxName: string,
            port: number,
            options: { hostname: string; name?: string; token?: string },
        ) {
            if (!Number.isInteger(port) || port < 1024 || port > 65535) {
                throw new Error(`Invalid port number: ${port}`);
            }
    
            if (options.token && !/^[a-z0-9_]{1,16}$/.test(options.token)) {
                throw new Error(`Invalid token: ${options.token}`);
            }
    
            const tokens = await this.readPortTokens();
            const token =
                options.token ?? tokens[port]?.token ?? generateToken();
            tokens[port] = { token, name: options.name };
            await this.ctx.storage.put(PORT_TOKENS, tokens);
    
            return {
                url: previewUrl(port, sandboxName, token, options.hostname),
                port,
                name: options.name,
            };
        }
    }

    该方法以 0.12 使用的格式存储令牌,并返回与 0.12 相同的 URL。与 0.12 不同,它不会启动容器。0.12 从自己的存储读取 sandbox 名称,因此请从 Worker 传入名称:

    src/index.tsts
    const sandbox = env.Sandbox.getByName(name);
    const { url } = await sandbox.exposePort(name, 8080, {
        hostname: "example.com",
    });
  3. 将其他端口方法添加到类:

    src/index.tsts
    export class MySandbox extends DurableObject<Env> {
        // ...
    
        async unexposePort(port: number): Promise<void> {
            const tokens = await this.readPortTokens();
            delete tokens[port];
            await this.ctx.storage.put(PORT_TOKENS, tokens);
        }
    
        async getExposedPorts(sandboxName: string, hostname: string) {
            const tokens = await this.readPortTokens();
    
            return Object.entries(tokens).map(([port, { token }]) => ({
                url: previewUrl(Number(port), sandboxName, token, hostname),
                port: Number(port),
                status: "active" as const,
            }));
        }
    
        async isPortExposed(port: number): Promise<boolean> {
            const tokens = await this.readPortTokens();
            return port in tokens;
        }
    
        async validatePortToken(
            port: number,
            token: string,
        ): Promise<boolean> {
            const tokens = await this.readPortTokens();
            const expected = tokens[port]?.token;
    
            if (expected === undefined) {
                return false;
            }
    
            // Compare in constant time, so timing does not reveal the token.
            const encoder = new TextEncoder();
            const a = encoder.encode(expected);
            const b = encoder.encode(token);
    
            if (a.byteLength !== b.byteLength) {
                return false;
            }
    
            return crypto.subtle.timingSafeEqual(a, b);
        }
    }

    0.12 的 getExposedPorts() 和 isPortExposed() 仅在当前容器中运行过 exposePort() 后才报告端口。这些方法报告每个已存储的端口,因为 Durable Object 会将请求转发到任何已存储的端口。

    0.12 destroy() 会删除令牌。在代码调用 destroy() 以结束 sandbox 的地方,也请调用 this.ctx.storage.delete(PORT_TOKENS)。

  4. 在 Worker 中路由预览主机名。在 0.12 中,proxyToSandbox() 在其他路由之前运行:

    src/index.ts (0.12)ts
    export default {
        async fetch(request: Request, env: Env): Promise<Response> {
            const proxied = await proxyToSandbox(request, env);
    
            if (proxied) {
                return proxied;
            }
    
            // Your other routes.
        },
    };

    在 1.0 中,Worker 从主机名读取端口、sandbox 名称和令牌,并检查令牌:

    src/index.ts (1.0)ts
    // <port>-<sandbox name>-<token>, as 0.12 issued preview hostnames.
    const PREVIEW = /^(\d{4,5})-([a-z0-9-]{1,63})-([a-z0-9_]{1,63})\./;
    
    // The Durable Object forwards to the port in this header.
    function toPort(request: Request, port: number): Request {
        const headers = new Headers(request.headers);
        headers.set("X-Sandbox-Port", String(port));
        return new Request(request, { headers });
    }
    
    export default {
        async fetch(request: Request, env: Env): Promise<Response> {
            const url = new URL(request.url);
            const preview = PREVIEW.exec(url.hostname);
    
            if (preview) {
                const [, portText, name, token] = preview;
                const port = Number(portText);
                const sandbox = env.Sandbox.getByName(name);
    
                if (!(await sandbox.validatePortToken(port, token))) {
                    return new Response("Not found", { status: 404 });
                }
    
                return sandbox.fetch(toPort(request, port));
            }
    
            // Your other routes.
        },
    } satisfies ExportedHandler<Env>;

    该表达式像 0.12 一样拆分第一个标签。端口在第一个连字符处结束,令牌在最后一个连字符之后开始。错误的令牌或没有令牌的端口会得到 404,与 0.12 一样。

    Durable Object 信任 X-Sandbox-Port 标头,toPort() 会替换访问者发送的任何值。只通过 toPort() 向 Durable Object 的 fetch() handler 发送请求,或先移除该标头,如 迁移浏览器终端 所述。

  5. 从 Durable Object 的 fetch() handler 转发请求:

    src/index.tsts
    export class MySandbox extends DurableObject<Env> {
        // ...
    
        async fetch(request: Request): Promise<Response> {
            const container = this.ctx.container;
            const port = Number(request.headers.get("X-Sandbox-Port"));
    
            // Preview requests do not start the container.
            if (!container?.running) {
                return new Response("The sandbox is not running", {
                    status: 503,
                });
            }
    
            const url = new URL(request.url);
            url.protocol = "http:";
            const forwarded = new Request(url, request);
            forwarded.headers.delete("X-Sandbox-Port");
    
            try {
                const tcpPort = container.getTcpPort(port);
                const response = await tcpPort.fetch(forwarded);
    
                if (response.webSocket) {
                    return bridge(response);
                }
    
                return response;
            } catch {
                return new Response("Nothing is listening on this port", {
                    status: 503,
                });
            }
        }
    }

    从 保持 WebSocket 连接打开 复制 bridge() 函数。仅经过 Durable Object 的 WebSocket 不会保持容器运行,因此该桥接会接受两端。

    服务器在 Host 标头中收到预览主机名。0.12 发送 localhost:<PORT>,并将预览主机名放在 X-Forwarded-Host 中。如果你的服务器检查任一标头(例如 Vite 使用 server.allowedHosts ↗),请更新其配置。

  6. 替换 Worker 中的每个 wsConnect() 调用。在 0.12 中,它将 WebSocket 连接到端口:

    src/index.ts (0.12)ts
    if (request.headers.get("Upgrade") === "websocket") {
        return getSandbox(env.Sandbox, name).wsConnect(request, 8080);
    }

    在 1.0 中,通过 toPort() 将请求发送到同一个 fetch() handler:

    src/index.ts (1.0)ts
    if (request.headers.get("Upgrade") === "websocket") {
        return env.Sandbox.getByName(name).fetch(toPort(request, 8080));
    }

    该请求不会启动容器,因此请在页面连接之前启动服务器。因为此路由没有令牌,请保留 Worker 在调用前运行的检查。

对正在运行的预览会发生什么

就地部署切换时,Durable Objects 会保留 portTokens 键。切换后,预览 URL 的行为如下:

请求 结果
0.12 签发的 URL 在新容器中该端口上有服务器监听之前返回 503,然后到达该服务器。部署后的短时间内,它仍可能到达 0.12 服务器。
容器再次启动后的任何 URL 无需再次调用 exposePort() 即可到达服务器。0.12 会返回 410,直到你再次暴露该端口。
错误的令牌,或没有令牌的端口 返回 404。
unexposePort() 之后的 URL 返回 404。

如果你 并排迁移 sandbox,copyFrom0x() 会将令牌复制到 1.0 类。在 Worker 的预览路由中,用 await sandboxFor(env, name) 获取 stub。

检查预览 URL

部署到预发布 Worker,并在 0.12 中有预览 URL 的 sandbox 中启动服务器。请求该 URL:

curl https://8080-ada-<TOKEN>.example.com/

响应来自你的服务器。用错误令牌请求同一端口:

curl https://8080-ada-wrong.example.com/ --write-out " %{http_code}\n"
Not found 404

相关资源

这篇文档对您有帮助吗?