camoufox

package module
v1.0.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2026 License: Apache-2.0 Imports: 10 Imported by: 0

README

camoufox

github.com/go-sdk/camoufox 提供 Camoufox 远程 Firefox 连接、独立会话及登录快照管理。页面操作直接使用原生 Playwright API。

New 仅保存配置,Connect 启动预先安装的本地驱动并建立远程连接。SDK 的职责和业务侧约束见支持边界。

环境要求与安装

go get github.com/go-sdk/camoufox
组件 版本或要求
运行平台 仅支持 linux/amd64、linux/arm64
Go 1.27.0 或更新版本
基础库 github.com/go-sdk/core v1.6.8,使用 errx 和 codec/json
Go Playwright 包 github.com/mxschmitt/playwright-go v0.6201.1
客户端驱动 Playwright 1.62.1
远程协议 Playwright 1.62.x,主、次版本必须一致
参考镜像 starudream/camoufox:1.0.1,服务端 Playwright 1.62.0

参考镜像的配置见镜像说明。上表是源码中的版本契约,连接及浏览器行为需在部署环境验证。

本仓库在 go.mod 中通过 replace 固定 Playwright Go 的实际源码版本,使 go get -u -v all 可以更新其他依赖而不改变驱动协议。该规则仅在本仓库作为主模块时生效。下游应用由自己的模块依赖图选定 Playwright Go 版本;若需要固定到表中的版本,应在自己的主模块中添加相同的 replace,并使用配套驱动。

集合依赖沿用 Playwright 的 golang-set/v2,当前使用 v2.8.0,通过 exclude 排除引入 MongoDB 驱动的 v2.9.0,不替换为 v3。该规则只在本模块作为主模块时生效;下游应用若执行依赖升级,需在自己的 go.mod 中配置相应排除规则。未来 v2 版本仍需重新检查依赖,Playwright 升级还需核对远程协议版本。

提前准备驱动

客户端需要配套驱动和 Node.js,无需 Python 或本地浏览器。在部署准备阶段显式执行:

PLAYWRIGHT_DRIVER_PATH=/opt/camoufox-driver \
  go run github.com/mxschmitt/playwright-go/cmd/playwright@v0.6201.1 --version

该上游命令准备驱动和 Node.js,并显示驱动版本;不会安装浏览器。不要追加 install,否则可能下载本地浏览器。也可以在独立的部署程序中调用 playwright.Install(&playwright.RunOptions{DriverDirectory: directory, SkipInstallBrowsers: true})。

生产或离线环境应在相同目标 Linux 架构上准备驱动,连同业务程序交付。默认驱动目录包含 package/ 和 node。如果准备阶段设置了 PLAYWRIGHT_NODEJS_PATH,还需交付该外部 Node.js,并在运行时设置相同用途的路径。驱动目录与登录快照目录相互独立。

配置 说明
WithDriverDirectory(path) 显式驱动目录,优先于环境变量
PLAYWRIGHT_DRIVER_PATH 未指定目录时由上游读取
PLAYWRIGHT_NODEJS_PATH 使用预装 Node.js
PLAYWRIGHT_CLI_PATH 使用外部管理的 cli.js,必须与 Go 包的驱动版本一致

未指定驱动目录时使用上游 Linux 缓存目录 ~/.cache/ms-playwright-go/1.62.1。

首次连接与手动登录

以下函数创建一个可在 KasmVNC 中操作的页面,业务确认登录后按回车保存快照。调用方传入真实业务登录地址,自己判断登录是否成功。

package example

import (
	"bufio"
	"fmt"
	"os"

	"github.com/go-sdk/camoufox"
	"github.com/go-sdk/core/errx"
	"github.com/mxschmitt/playwright-go"
)

func login(endpoint, loginURL, statePath string) (result error) {
	client := camoufox.New(camoufox.WithEndpoint(endpoint))
	defer func() {
		result = errx.Join(result, client.Close())
	}()
	if err := client.Connect(); err != nil {
		return err
	}
	session, err := client.NewSession(playwright.BrowserNewContextOptions{
		NoViewport: playwright.Bool(true),
	})
	if err != nil {
		return err
	}
	page, err := session.Context().NewPage()
	if err != nil {
		return err
	}
	if _, err := page.Goto(loginURL); err != nil {
		return err
	}
	if _, err := fmt.Fprintln(os.Stdout, "请在 KasmVNC 中完成并确认登录,然后按回车保存快照。"); err != nil {
		return err
	}
	if _, err := bufio.NewReader(os.Stdin).ReadString('\n'); err != nil {
		return err
	}
	return session.SaveState(statePath, playwright.BrowserContextStorageStateOptions{
		IndexedDB: playwright.Bool(true),
	})
}

参考镜像默认 Playwright 地址为 ws://127.0.0.1:3000/ws,KasmVNC 地址为 https://127.0.0.1:8443。在 KasmVNC 中操作上述会话创建的页面;关闭会话会关闭这些页面。KasmVNC 的认证不保护独立的 Playwright 端口。

恢复快照与代理

在已经连接的 client 上创建会话,StorageStatePath 指向客户端机器上的文件,浏览器容器无需挂载该文件。代理地址须从浏览器容器可达,密码等参数由调用方安全提供。

session, err := client.NewSession(playwright.BrowserNewContextOptions{
	NoViewport:       playwright.Bool(true),
	StorageStatePath: playwright.String(".state/login.json"),
	Proxy: &playwright.Proxy{
		Server:   proxyServer,
		Username: playwright.String(proxyUsername),
		Password: playwright.String(proxyPassword),
	},
})
if err != nil {
	return err
}
page, err := session.Context().NewPage()
if err != nil {
	return err
}
// 使用 page 的原生 Playwright API 执行业务操作。
_ = page

无需代理时省略 Proxy;首次登录时省略 StorageStatePath。未指定代理的会话不继承其他会话的代理。SDK 保留 Playwright 的默认上下文选项,不自动开启 NoViewport 或忽略 HTTPS 错误。

快照文件不存在、JSON 损坏或恢复失败都会返回错误,不静默创建未登录会话。不要同时指定 StorageState 和 StorageStatePath。内联 StorageState 仍使用上游 Go 类型,需要完整 IndexedDB 时使用文件路径。

代理只影响上下文网络请求,不会重新生成浏览器启动阶段的指纹、GeoIP 或 WebRTC 信息;出口一致性需要业务独立设计和验证。

登录快照

err := session.SaveState(".state/login.json", playwright.BrowserContextStorageStateOptions{
	IndexedDB: playwright.Bool(true),
})
  • SaveState(path) 默认保存 Cookie 和 Local Storage;IndexedDB 需显式启用。
  • 可选参数最多一个,其 Path 必须留空,目标路径以第一个参数为准。
  • 保存保留上游写出的原始 JSON。恢复只读取并校验一次源文件,创建上下文后通过 0600 临时文件将已校验的内容交给原生 SetStorageState,使用后删除临时文件。快照校验使用 core/codec/json,不经过上游 Go 状态结构体,避免丢失 IndexedDB。
  • 新建父目录使用 0700,同目录临时文件使用 0600。写入、格式校验和文件同步成功后重命名替换目标;替换前失败保留旧快照,并尝试清理临时文件。
  • Linux 同目录重命名提供原子替换;这不是文件系统断电恢复保证。
  • 已有目录权限由调用方管理。不同 Session 或进程写同一路径时,调用方应串行协调;SDK 仅串行化同一 Session 的保存。
  • 不在关闭、进程退出或断线时隐式保存,业务负责保存时机和过期判断。

快照包含登录凭证,应放在受控持久化目录;本仓库忽略 .state/。原生选项 Credentials 还可包含虚拟 WebAuthn 凭证,调用方应按凭证文件管理。StorageState 的范围见支持边界。

Client 配置

client := camoufox.New(camoufox.WithEndpoint(endpoint), camoufox.WithConnectTimeout(30*time.Second), camoufox.WithHeaders(headers), camoufox.WithDriverDirectory(driverDirectory))
  • WithEndpoint 默认使用 ws://127.0.0.1:3000/ws,接受完整 ws://、wss:// 地址,保留反向代理子路径和查询参数,不拼接 /ws;不接受 URL 用户信息或片段。
  • WithConnectTimeout 默认 30 秒,零表示不限时,负数返回 ErrInvalidOptions。超时覆盖远程连接与协议初始化,不包含本地驱动启动;页面操作超时由原生 Playwright 选项管理。
  • WithHeaders 在创建客户端时复制握手 Header,与页面请求 Header 独立,认证值由调用方安全提供。
  • WithDriverDirectory 指定已准备好的驱动目录,未指定时遵循上游环境变量和缓存路径。

使用期间不要重新应用 Options 或并发修改配置。IsConnected 检查本地驱动进程及当前连接状态,不发起网络探测。

生命周期

Session.Close 关闭自己的上下文和页面。Client.Close 清理所管理的会话、断开本客户端远程连接并停止本地驱动,浏览器服务由容器管理。清理客户端或失效连接时,若已有会话正在关闭,先等待该操作完成并合并错误;检测到本地驱动退出时停止等待。直接并发重复调用 Client.Close 或 Session.Close 不会等待首次调用,应以首次调用返回作为收尾完成依据。

同一客户端并发 Connect 时,后来的调用返回 ErrConnecting。Close 会阻止正在创建的连接或会话向调用方发布,并等待并发 Connect 完成,合并其资源清理错误;普通连接失败本身不作为关闭错误。与 NewSession 并发时,仍需等待该调用返回,确认其失败清理已完成。原生 Page、Context 的并发和事件行为遵循上游接口,不应在事件回调中同步调用客户端生命周期方法,应投递到业务 goroutine 处理。

断线或容器重启后,旧 Session、Context 和 Page 失效。未关闭的 Client 可以显式再次 Connect(),清理旧连接,再用 NewSession 恢复快照、重新创建页面并验证登录。已调用 Client.Close() 时需要重新 New()。SDK 不自动重连或重放页面操作,业务自行决定重试,避免重复提交。

本地驱动退出后,SDK 的 IsConnected 返回 false,NewSession 返回 ErrNotConnected,旧会话的 SaveState 返回 ErrSessionClosed;显式 Connect 会重新启动预装驱动。SDK 同步进程存活查询与独立查询句柄的释放,不另行等待或回收上游子进程。此检查与后续调用之间仍存在进程退出的时间窗口。

当前 Playwright Go 版本未在本地驱动退出时完整关闭内层远程协议连接,因此原生 Browser.IsConnected、Page 的关闭状态和事件可能仍保留旧值,已经挂起的原生 Page、Context 调用也可能无法返回;SDK 没有公开的上游接口可以解除这些等待。显式重连仅恢复后续新对象的使用,不保证旧调用结束;对故障后必须完整回收的任务,应在独立工作进程中运行,由业务监督进程重启。这一上游限制仍需修复。

错误处理

if errx.Is(err, camoufox.ErrNotConnected) {
	// 由业务决定何时重连并重建会话。
}
if errx.Is(err, playwright.ErrTimeout) {
	// 上游超时类别被安全保留。
}

SDK 使用 core/errx 创建、合并和判断错误,也兼容标准库 errors.Is,完整类别见 error.go。上游错误可能包含端点、Header、代理密码或快照内容,SDK 只返回固定操作类别,并保留 playwright.ErrTimeout、playwright.ErrTargetClosed 类别,不透出上游错误正文。Session.Close 将已关闭远程对象视为清理成功。

调用原生 Page、Context 方法取得的错误由调用方管理。SDK 不启用上游调试日志;调用方设置 DEBUG 等调试环境变量时,上游可能输出协议和请求内容,应自行控制日志范围。

支持边界

  • 仅支持远程 Firefox Playwright 连接,页面操作直接使用原生 Context、Page 和选项类型。
  • SDK 管理客户端连接、独立会话和登录快照;容器负责浏览器进程、指纹、KasmVNC 和 Playwright 服务。
  • 不启动本地浏览器,不自动下载安装依赖,不管理容器或网站业务流程。
  • StorageState 不包含 Session Storage、完整用户目录、扩展、页面或完整浏览器配置,网站是否能恢复登录需要实际验证。
  • 业务负责登录成功判断、快照保存时机和过期判断、代理出口一致性及重试策略。

开发

以下命令在支持的 Linux 环境中执行;其他系统可显式设置 GOOS=linux、GOARCH=amd64 或 arm64 做交叉编译和静态检查,无法直接运行生成的 Linux 程序。

make fmt
make lint
make build
git diff --check

make lint 先整理模块依赖,再检查包括 integration 标签在内的源码;make build 仅编译生产包。以上命令不执行测试、不启动驱动、不连接浏览器。空仓库尚未提交时,可设置 GOFLAGS=-buildvcs=false 执行静态检查与编译。

测试流程

普通单元测试

在支持的 Linux 环境的模块目录执行:

make test

覆盖客户端配置、参数校验、错误分类、驱动状态、并发关闭和清理顺序,以及快照的格式、原始内容、权限、失败清理和源文件替换。使用 core/testx 和临时目录,不安装或启动驱动,不需要 Docker。命令开启竞态检测,运行环境需要 Go 及支持 CGO 的 C 编译器;首次整理或下载 Go 依赖可能联网。

Compose 浏览器集成测试

先启动 Docker Desktop、OrbStack 或其他 Docker Engine,确认当前 Docker context 可用:

docker version
docker compose version

在模块目录执行:

make test-compose

宿主机只需要 Docker、Compose 和 make,无需本地 Go、Node.js 或浏览器驱动。该命令会:

  1. 使用 starudream/camoufox:1.0.1,构建基于 golang:1.27.2-bookworm 的测试镜像。
  2. 在测试镜像构建阶段,根据 go.mod 中的 Playwright 包准备配套驱动和 Node.js,将 Node 权限设为 0755,供运行用户 1000:1000 执行,再编译带竞态检测的测试执行器;此时不执行测试。
  3. 启动 Camoufox,等待 /json 返回预期的 /ws 端点,再依次执行根包的普通单元测试和 tests/ 的浏览器集成测试;任一阶段失败立即返回非零状态。
  4. 输出 Go 测试结果;测试完成、失败或正常中断后,清理本次 Compose 项目的容器和网络。测试失败会使命令返回非零状态,已有失败状态不会被清理结果覆盖。

首次运行需要联网拉取镜像、Go 模块、驱动和 Node.js。测试运行阶段使用 Compose 内部网络,不发布宿主机端口、不挂载已有登录快照。测试网页通过 Playwright 路由拦截提供,不访问真实网站;KasmVNC 密码在浏览器容器内随机生成,不写入配置或输出。镜像构建上下文仅包含 Go 源码、模块文件和测试 Dockerfile,不包含 .git 或 .state/。

用例 验证内容
TestIntegrationConnectionAndIsolation 连接、页面操作、同客户端会话隔离、多客户端隔离及重复关闭
TestIntegrationPageFileTransfer 通过测试页面上传本地文件,并将远程浏览器下载保存到客户端
TestIntegrationStorageState Cookie、Local Storage、IndexedDB 保存恢复,默认不保存 IndexedDB,文件及新建目录权限
TestIntegrationStateFailures 缺失、损坏及无效快照拒绝恢复,恢复失败清理上下文,保存失败清理临时文件并保留已有数据
TestIntegrationReconnect 主动断开远程连接后旧对象失效,显式重连并从快照重建会话
TestIntegrationDriverExit Linux 下终止本次创建的本地驱动,检查 SDK 状态及显式重连后的快照恢复

浏览器测试位于 tests/,导入 SDK 并使用公开 API 验证完整流程;重连用例通过原生 Session.Context().Browser() 断开当前连接,不访问 SDK 私有字段。Linux 驱动退出用例通过 /proc 元数据定位本次连接新建的子进程并终止,不读取进程命令行或环境变量,不验证旧原生调用能否解除等待。Compose 与 Dockerfile 放在同一目录,构建上下文仍为模块根目录。

以上用例测试受控页面上的存储行为,不证明真实网站登录、代理出口、KasmVNC 人工交互或容器崩溃恢复。浏览器测试源文件使用 integration 构建标签,并要求 CAMOUFOX_INTEGRATION_TESTS=1;普通 make test 不编译或运行浏览器用例。

保留现场排查

需要查看失败容器时,使用下面的分步命令;它们不会自动执行收尾清理:

docker compose -f tests/compose.yaml build tests
docker compose -f tests/compose.yaml up --abort-on-container-exit --exit-code-from tests
docker compose -f tests/compose.yaml ps -a
docker compose -f tests/compose.yaml logs --no-color camoufox tests

排查完成后清理:

docker compose -f tests/compose.yaml down --remove-orphans

默认项目名为 camoufox-sdk-test,清理仅作用于该项目。需要并行运行多组测试时,分别设置不同的 COMPOSE_PROJECT_NAME,同组命令使用同一项目名。清理保留镜像及构建缓存;快照只使用测试执行器内的临时目录。

在支持的 Linux 环境中仅做测试源码编译检查时,可以执行以下命令;它不会运行任何用例:

go test -c -race -o /tmp/camoufox-unit-tests .
go test -c -race -tags=integration -o /tmp/camoufox-integration-tests ./tests

静态检查、测试源码编译和 docker compose config 解析成功均不代表测试通过,实际结果以自行执行测试时的输出和退出状态为准。

项目地图 · 协作规范 · Apache-2.0

Documentation

Overview

Package camoufox 封装 Camoufox 的远程 Firefox Playwright 连接、会话和登录快照。

页面操作使用原生 Playwright API;本地驱动需提前准备,SDK 不自动安装依赖。 仅支持 linux/amd64 和 linux/arm64。 断线后由调用方显式重连并重新创建会话,不自动重放页面操作。

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrInvalidEndpoint = errx.New("invalid websocket endpoint")
	ErrInvalidOptions  = errx.New("invalid options")
	ErrClientClosed    = errx.New("client is closed")
	ErrConnecting      = errx.New("client is connecting")
	ErrNotConnected    = errx.New("client is not connected")
	ErrDriverStart     = errx.New("could not start playwright driver; prepare the matching driver first")
	ErrDriverStop      = errx.New("could not stop playwright driver")
	ErrConnect         = errx.New("could not connect to remote browser")
	ErrDisconnect      = errx.New("could not disconnect from remote browser")
	ErrNewSession      = errx.New("could not create browser context")
	ErrSessionClosed   = errx.New("session is closed")
	ErrSessionClose    = errx.New("could not close browser context")
	ErrStatePath       = errx.New("storage state path is required")
	ErrStateSave       = errx.New("could not save storage state")
	ErrStateRestore    = errx.New("could not restore storage state")
)

Functions

This section is empty.

Types

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client 管理一个本地驱动及其远程连接,必须通过 New 创建,创建后不可复制。

func New

func New(opts ...Option) *Client

New 仅保存配置,不启动驱动、安装依赖或建立连接。

func (*Client) Close

func (c *Client) Close() error

Close 关闭本客户端会话、断开连接并停止驱动,不保存快照。 首次调用等待正在进行的 Connect 清理完成并合并清理错误;并发重复关闭不等待首次清理。

func (*Client) Connect

func (c *Client) Connect() error

Connect 启动预先安装的驱动并连接 Firefox 服务;已连接时直接返回。 断线后可显式重连,旧会话会被清理;Close 后需要新建 Client。

func (*Client) IsConnected

func (c *Client) IsConnected() bool

IsConnected 查询本地驱动及当前远程连接,不发起网络探测。

func (*Client) NewSession

func (c *Client) NewSession(options ...playwright.BrowserNewContextOptions) (*Session, error)

NewSession 创建上下文,支持零个或一个原生选项;代理由远程浏览器连接。 StorageStatePath 通过原始 JSON 恢复,避免 Go 状态模型丢失 IndexedDB。

type Option

type Option func(*Client)

func WithConnectTimeout

func WithConnectTimeout(timeout time.Duration) Option

WithConnectTimeout 设置远程连接超时;零表示不限时,负数在 Connect 时返回错误。

func WithDriverDirectory

func WithDriverDirectory(directory string) Option

WithDriverDirectory 指定预先安装的驱动目录;空值遵循上游环境变量和缓存路径。

func WithEndpoint

func WithEndpoint(endpoint string) Option

WithEndpoint 设置完整的 WebSocket 地址,保留路径和查询参数,不自动追加 /ws。

func WithHeaders

func WithHeaders(headers map[string]string) Option

WithHeaders 设置 WebSocket 握手 Header;创建客户端时复制,不持有调用方的 map。

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session 管理一个独立上下文及其页面,创建后不可复制。

func (*Session) Close

func (s *Session) Close() error

Close 幂等关闭会话及其页面,不隐式保存登录快照。

func (*Session) Context

func (s *Session) Context() playwright.BrowserContext

Context 返回原生上下文,供调用方创建页面和使用完整 Playwright API。 关闭或断线后返回的旧对象失效,调用方应重新创建会话。

func (*Session) SaveState

func (s *Session) SaveState(path string, options ...playwright.BrowserContextStorageStateOptions) (result error)

SaveState 显式保存原始登录快照;可选原生参数支持 IndexedDB,Path 必须留空。 使用同目录 0600 临时文件替换目标;失败时保留旧快照,不在关闭时自动保存。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL