-
Notifications
You must be signed in to change notification settings - Fork 0
Web Console
带登录的网页界面:看运行状态、查请求流、试一试某个 URL、改配置、管集群节点、 装卸可选组件。
ipclick run -w
# 同一目录起第二个实例:gRPC 与 Web 两个端口都要岔开
ipclick run --port 9628 --web-port 9531 -w==============================================================
IPClick Web 管理端: http://127.0.0.1:9527/
用户名: admin
密码: N8mSKkdPbpiGzB128At3
⚠️ 该密码为本次启动随机生成,重启后失效。
==============================================================
[WEB]
enabled = false # 命令行 -w / --web 会覆盖它
port = 9527 # 命令行 --web-port 会覆盖它
host = "127.0.0.1" # 默认只监听本机登录凭据是机密,走 .env:
IPCLICK_WEB_USER=admin
IPCLICK_WEB_PASSWORD=<一串够长的>都不设的话每次启动随机生成密码并打印到控制台。 不预置 admin/admin 是刻意的—— 默认弱口令是这类管理界面被打穿的头号原因。
⚠️ 默认只监听127.0.0.1。 这个界面后面就是一个能代发任意请求的服务, 不该默认对外。要远程访问请用 SSH 隧道:ssh -L 9527:127.0.0.1:9527 user@server或者放在做了 TLS 终止的反向代理之后——它本身是明文 HTTP,密码会在网络上裸奔。
可信局域网里自用(拿手机看看跑得怎么样)可以
ipclick run -w --web-lan(等价于--web-host 0.0.0.0,也可以用--web-host只绑某一张网卡)。 此时启动会打两条明文告警。跨网段仍然请走隧道或反代。
三栏 CSS Grid:
┌────────┬────────────────────────┬──────────┐
│ 左导航 │ 主内容 │ 右状态栏 │
│ 六个页面│ │(仅总览页)│
│ 主题切换│ │ │
└────────┴────────────────────────┴──────────┘
把"当前状态"常驻在右栏,主区留给要动手的内容——否则总览页会把服务器信息、各适配器、 渲染引擎、集群、最近请求全挤在一条竖线上,只能一路往下滚。
窄屏会分两级降级:先把右栏移到主内容下面,再把左导航压成顶部横条。
主题是两态的:亮 / 暗,选择记在 localStorage,刷新不丢。服务端默认值是
[WEB].theme,而浏览器里点过的那一下优先于它——反过来的话,用户每刷新一次页面,
自己刚选的主题就会被配置文件推翻一次。
刻意没有"跟随系统"这一档:它靠 CSS 的 prefers-color-scheme,而那一位取决于浏览器
读不读得到桌面偏好——Linux 上要 GTK 或 xdg-desktop-portal 配好才认,读不到就静默按亮色
处理。一个在半数机器上不生效、失败时又毫无迹象的选项,比没有这个选项更糟。
主区里每 5 秒自动更新的只有这两块(只换那一块,不重载整页):
- 请求统计:总数、成功率、平均耗时、在途与峰值、累计流量、运行时长
- 状态码分布条
下面这几块随整页渲染,要刷新页面才更新:
- 各适配器的请求数 / 成功 / 失败 / 平均耗时 / 流量
- 最近请求
- 集群拓扑与各节点健康状况。每行带一个「摘除 / 恢复」按钮,可以就地手动把某台
节点摘出轮询(POST
/action,带 CSRF)——发版或排查时不用改配置文件
右栏常驻:
- 服务端:监听地址、本节点 id、运行模式、worker 数、默认适配器、压缩策略、配置文件路径
- 安全:TLS、令牌鉴权、SSRF 两个开关、页内 JS、集群内部鉴权
- 链路记录:数据来源、内存缓冲、落盘条数与文件大小、丢弃条数、保留天数
- 限流:按 host 的并发与 QPS 上限
- 可选组件:五个 extras 的状态速览,点进去就是组件页
组件的状态是两级的(包 / 浏览器本体),因为处理动作不同:一个是 pip install,
一个是 camoufox fetch,混成"不可用"会让人查错方向。
每个请求一条记录,实时刷新:默认每 5 秒,页面上可切 关闭 / 1 秒 / 5 秒 / 30 秒。 档位会写回地址栏,所以收藏或分享出去的链接带着这个设置。
刷的是局部:只替换表格那一块的 HTML,不重载整页。整页重来会丢滚动位置、
冲掉正在填的过滤条件、还会白闪一下。渲染仍然在服务端(/fragment/trace),
所以不存在"JS 里那份模板和 Python 这份对不上"的问题。
| 列 | 说明 |
|---|---|
| 时间 | 完成时刻 |
| 状态 | 状态码。传输故障显示成红色的「失败」标签(/api/trace 的 JSON 里才是 -1) |
| 方法 | GET / POST / … |
| URL |
[TRACE].record_url = false 时只显示 host |
| 适配器 | 实际用了哪个(browser 会显示解析后的具体引擎) |
| 节点 | 实际执行的节点 id —— 集群转发是否生效看这里 |
| 耗时 | 毫秒 |
| 大小 | 响应体字节数 |
| (末列) | 标记位:转发 / 流式 / 重试 N / 排队 Nms |
支持按状态、适配器、URL 关键词、条数筛选(节点只是表格里的一列,不能筛)。 状态可选:全部 / 只看失败 / 2xx / 3xx / 4xx / 5xx / 连接失败。
关键词是字面匹配。 URL 里
%和_很常见(/api_v2/、utm_source=), 底层 SQL 的LIKE会把它们当通配符,所以查询前做了转义——搜api_v2不会匹配到apiXv2。
开了 [TRACE].sqlite_enabled 才能查历史和跨天统计;不开就只有内存里最近
memory_size 条(默认 500),进程重启即丢。详见链路记录。
输入一个 URL 直接发请求,看链路和源码。适合验证配置改对了没有、 某个站点用哪个适配器能过。
能填的和 SDK 的 request() 基本一一对应:方法、适配器、超时、重试次数与退避、
请求头、cookie、查询参数、请求体(raw 或 JSON)、允许的状态码、verify、
是否跟随重定向、指纹伪装(仅 curl_cffi)、是否走代理、目标节点,以及
automation_config 与 automation_script(后者需要服务端 allow_scripts = true)。
结果显示状态码、耗时、响应头、执行节点、重试次数,以及响应体源码。
浏览器 DevTools 里对着请求「复制为 cURL」,粘进输入框就自动填好 URL / 方法 / 请求头 / 请求体。手动把十几个 header 逐个拆进表单既慢又容易漏,而漏掉一个 header 往往就是"为什么我用 IPClick 抓不到、浏览器却可以"的答案。
认得 -X / -H / -d / --data-raw / -b / -A / -e / -m / --url 等常用参数,
以及 --compressed、-sSL 这类合并短开关。认不出或没法对应的会明确列出来
(比如 -F 文件上传、-k 跳过证书校验)——静默丢掉一个参数比不支持它更糟,
用户会以为已经导入了。
短参数贴值的写法一并认(-XPOST、-m30、-H'X: y'),值里带 = 也不影响:
-H'Cookie: sid=abc'、-d'user=alice&p=1'、-b'sid=abc'。
$'...'(DevTools 在值里有转义字符时的输出)里,字面中文、\xNN 字节转义、\uHHHH
三种都能还原。不带协议但带查询串的地址(example.com/api?q=1)也认。
这几类写法都是 DevTools 实际会输出的,而解析错了往往不会报错、只是悄悄换了意思:
= 属于短参数的值而不是"参数名=值"的分隔符,所以 -H'Cookie: sid=abc' 要整条保住;
$'{"name":"张三"}' 要还原成原文而不是 {"name":"??"};example.com/api?q=1 里的
= 在查询串而不在主机名里,所以它是网址、不该被判成"没找到网址"。
开了服务端转发时多一个"目标节点"下拉,选中后跳过负载均衡强制打到那一台。
这是为了验证新加的机器配对没有:按策略选就只能反复点、靠轮询碰运气命中, 节点一多完全没法用。刻意走内部路径而不改协议——点名是诊断能力,不该变成正式的 路由语义(那就要回答"点名的节点挂了要不要转移""能不能穿透多跳"这些问题)。
没有故障转移是有意的:点名就是点名,转到别的机器上会让这次验证失去意义。
失败时的报错也是人话,不是一坨 _InactiveRpcError 的 repr:
转发到节点 node-c 失败 —— UNAUTHENTICATED:缺少或无效的鉴权令牌。
集群内部鉴权不通过:两端的 IPCLICK_CLUSTER_SECRET 必须完全一致
(在一台机器上生成,原样复制到其余机器的 .env)
都是刻意的:
- 源码最多显示 256 KB。 再多浏览器渲染会卡,而看源码这件事看前几十 KB 基本够判断。
- 超时上限 120 秒。 页面是同步等结果的,让它能等十分钟等于给自己留一个占满 worker 的口子。
用的是 Post/Redirect/Get:提交后重定向再展示,刷新页面不会重复发请求。
五个可选 extras 的安装状态与装 / 卸,按「HTTP 适配器 / 浏览器渲染」分两组。
每个组件显示两级状态,两级都要看:
| 显示 | 含义 | 该做什么 |
|---|---|---|
| 未装 | Python 包不在 | 点「安装」,或 pip install "ipclick[xxx]"
|
| 缺本体 | 包装了,浏览器本体没下 | 点「下载浏览器本体」,或跑各自的 fetch/install |
| 本体未知 | 包在,但探不出本体状态(多半装坏了,或引擎依赖的子模块导不进来) | 重装这个组件,或手动跑一次它的 fetch / install |
| 可用 | 两级都就绪 | — |
"网页能在机器上装东西"值得停一下。 这里的判断是:一个已经能改配置、能代发任意 请求的管理端,再加上"装它自己声明过的那五个可选依赖"并没有实质性地扩大攻击面, 而省掉的"开一个终端 → 找到那个 venv → 敲对命令 → 重启进程"是真实的痛点。
放开这一项不等于放松要求,实现上守着六条:
-
包名全部来自白名单常量,绝不拼接用户输入。 表单里传来的 extra 必须能在组件
清单里查到,查不到直接拒绝。命令以列表形式交给
subprocess(shell=False), 连"引号转义写错"这个类别都不存在。 -
绑定当前解释器。 不依赖 PATH 上的
pip,也不依赖"当前激活的 venv"。装到 别的环境去比装不上更糟——装完页面还是看不到,人会以为这个功能坏了。 -
pip和uv pip两条路都支持,自动探测。 uv 创建的 venv 默认不装 pip (IPClick 自己的开发环境就是),此时python -m pip会报No module named pip; 反过来非 uv 环境里又没有uv命令。两者都没有时明确报错并给出手动命令。 -
长任务不占 HTTP 请求。
camoufox fetch要下约 1 GB,跑在后台线程里,页面 轮询状态并实时显示输出。否则请求超时、用户以为失败、然后重复点击叠加下载。 -
错误原样透出。 系统 Python 下没有写权限时 pip 会失败,那条
Permission denied本身就是答案,笼统的"安装失败"等于把答案藏起来。 -
卸载只卸 Python 包。
pip uninstall camoufox不会删掉~/.cache/camoufox里 那 1 GB 浏览器本体。界面把它的路径和体积摆出来让人自己决定——从网页上递归删 一个 GB 级目录是不可逆操作,风险和收益完全不成比例。
装的不是 ipclick[extra],而是从本机 ipclick 自己的元数据(Requires-Dist)读出
来的依赖列表。装 ipclick[extra] 会把 ipclick 自身拖进解析:不钉版本可能被"升级"
覆盖掉正在运行的这份(开发环境的可编辑安装尤其容易),钉了版本又要求该版本能在索引
上找到——本地开发版必然卡住。
元数据实在读不出来时会回落到 ipclick[extra]==<当前版本>,页面会把这件事写在
任务输出的第一行。这是兜底而非常态:走到这条路的本地开发版基本会失败,但至少失败
的原因是明说出来的。
探测用的是 importlib.util.find_spec() + invalidate_caches():不执行模块代码、
能立刻看到新装的包,而且能正确反映卸载。真正的 import 推迟到"要启动浏览器了"那一刻。
不能用模块级的 try: import X。 那样结论会在进程启动那一刻就固化:在终端里装完
camoufox,Web 端刷新多少次都还是"未装";卸载更是完全探测不出来——真 import 过的模块
留在 sys.modules 里,删掉磁盘上的包也不会让它消失。
页面上有手动「刷新状态」按钮;装 / 卸任务结束后也会自动刷一次,并同步适配器注册表 ——刚装好的 niquests 立刻就能在「试一试」里选。
刻意不做成"每次加载页面都重新探测":那要动 importlib.reload,在一个正在服务
请求的进程里重载 camoufox(它还牵着 playwright)风险很高。
改白名单内的行为配置,写回 ipclick.toml。
写入方式:定点文本替换 → 保留注释与格式;改动前留 .bak;
写临时文件再 os.replace → 断电不会留下半个配置文件。
每一项标了是否需要重启生效。日志级别、调试模式、only_errors、record_url
这些是热生效的;max_workers、监听地址这些要重启。
可改的一共 75 项,分 12 组:
| 分组 | 项 |
|---|---|
| 服务端 | gRPC / Web 端口与监听地址、worker 线程数、工作进程数、在途 RPC 上限、单连接并发流上限、响应压缩、异步模式、调试模式 |
| 日志 | 级别、输出位置、格式、单文件上限、保留个数 |
| 下载行为 | 单次请求超时、连接超时、跟随系统代理、流式分片大小 |
| 重试 | 最大次数、退避基数、退避指数、单次等待上限 |
| 连接池 | 连接池总上限(niquests 读它;curl_cffi 在异步模式下当作会话并发上限)、长连接保活上限(只有 niquests 读) |
| 按 host 限流 | 并发上限、等待超时、闲置回收、最多跟踪多少 host、QPS 上限、突发额度 |
| 浏览器渲染 | 启用开关、引擎、无头、并发页面上限、三个超时、内核、wait_until、视口、UA、语言、指纹跟随代理、沙箱开关、代理网关 |
| 代理 | 主机、端口、协议、隧道接入地址与三个隧道参数 |
| 链路记录 | 内存条数、SQLite 开关与路径、保留天数、只记失败、落盘队列容量、记录完整 URL |
| 集群 | 本节点 id、负载均衡、故障转移次数、转发超时、探活间隔与超时、摘除与恢复阈值 |
| Web 管理端 | 页面主题 |
| 客户端与压缩 | RPC 重试与退避、请求压缩策略与门槛 |
鉴权令牌、Web 密码、集群共享密钥各能生成一个随机值,只显示一次,页面上直接给出
可以粘进 .env 的那一行。
"只显示一次"是真的:服务端不保存、不写进任何文件,值只在那一次响应里出现过, 刷新就没了。这条规矩(机密不接受从本页写入)顺带让"不可再次查看"自动成立—— 没抄下来就再生成一个。
生成时会区分两类,因为它们的后续动作完全不同:
| 类型 | 提示 |
|---|---|
| 本机独有(鉴权令牌、Web 密码) | 生成即用,改完重启本进程即可,不用同步 |
| 集群共享密钥 | 必须原样复制到所有其他节点的 .env |
后者不说清楚就会出事:每台机器各自生成一个,派生出来的节点令牌互不匹配,
转发会全部 UNAUTHENTICATED。
节点管理是配置页的一个分页(/config?tab=cluster)。
/nodes 这个老地址会自动跳过去——节点管理本来就是集群配置的一部分。
集群节点列表的增删改,同样写回 ipclick.toml。提交前会校验地址格式、id 唯一性等,
校验不过就整批拒绝——写进去一半是最难排查的状态。
节点的 token 不在这里管。 令牌由集群共享密钥派生,见集群。
前提:单进程模式([SERVER].processes = 1,默认值)。多进程时 Web 只跑在
0 号进程上,只更新它会让各 worker 的路由与鉴权状态分裂,所以配置只写盘不热更新,
页面会明说"需重启 ipclick 才会在全部 worker 生效"。而"工作进程数"本身就是配置页
上可改的一项,所以这个前提很容易在不知不觉间失效。
单进程下保存会原地重建 ClusterConfig 与 NodePool:新节点立刻参与转发轮询,
被移除 / 改了地址的节点连接会被关掉。
开着服务端转发时,重建按 id 复用已有的节点状态——直接重建会把每个节点的健康计数 清零,那样"连续 N 次才切状态"的判定永远达不到,熔断与恢复双双失效。
热更新覆盖的是不涉及监听端口的部分:节点列表、权重、策略、阈值。监听地址属于 gRPC server 的构造参数,那个换不了——但改节点列表本来也不会动到端口。
DNS 发现模式(
[CLUSTER.discovery].mode = "dns")下节点是解析出来的,[CLUSTER].nodes只是占位,热更新只改策略与阈值、不动节点列表。
每行一个按钮,就地验连通性与集群内部鉴权,不发业务请求。
否则加完节点只能等真实流量转发过去才发现连不上,而那时错误已经混在业务失败里了。
结论分四种,因为排查方向完全不同:
| 结果 | 含义 | 去查什么 |
|---|---|---|
| 连不上 | 端口不通或对端没起来 | 进程、防火墙、地址写没写对 |
| 鉴权不通过 | 连上了,但令牌不匹配 | 各节点 .env 里的 IPCLICK_CLUSTER_SECRET 是否完全一致 |
| 通过(对方未设防) | 连上了,但对端根本没启用鉴权 | 那台机器任何人都能调,补上共享密钥 |
| 通过 | 连上了、令牌对、对端也开了鉴权 | 不用查了 |
为什么不能只用健康检查:grpc.health.v1 刻意免鉴权(编排系统的探针通常拿不到
密钥),所以在它眼里"那台机器没起来"和"起来了但我的令牌不对"长得一模一样。所以探两
层——健康检查回答"连得上吗",Ping RPC(走鉴权、不做任何业务动作)回答"令牌对吗"。
第三种情况需要对端自报一位 auth_required:探测成功本身分不清"我的令牌对"和
"它根本不验"。
对端没有
Ping会怎样? 会返回UNIMPLEMENTED。但能走到方法查找这一步, 说明鉴权拦截器已经放行了——所以页面会如实说"连得上、鉴权也通过,但对方不认识 这个方法",而不是误报成鉴权失败。滚动升级期间这个区分很重要。
地址栏里还没保存的地址也能直接测——加完一行想先试试通不通是最自然的动作, 非要先保存才能测就把流程割断了。
给 AI 代理用的技能包页:装它的命令、SKILL.md 全文、以及原文下载(/skill.md)。
技能包是一份随 wheel 分发的 Markdown,讲清楚什么时候该用 IPClick、--json 的输出
契约、以及几个最容易踩的坑。装完之后直接对代理说"用 ipclick 抓一下 …"即可,
不必每次再逐条解释。命令行侧对应 ipclick skill show / install / path,
详见命令行。
页面存在的理由和「从 curl 导入」一样:把一份要交给别的程序的东西摆出来给人看一眼。 代理读到的到底是什么内容,不该只能靠翻源码确认。
不可从网页修改:
-
[SECURITY]全部 —— 鉴权令牌、TLS、SSRF 防护 - Web 管理端自己的登录凭据
- 集群共享密钥与各节点 token
[BROWSER].allow_scripts
理由:这些是安全边界本身。一个能改安全边界的管理界面,等于把整条防线的强度降到 "这个界面的登录有多牢"。攻破 Web 登录不该等于能关掉 SSRF 防护、能拿到令牌、 能打开任意 JS 执行。这几项只能改配置文件 + 重启——那需要机器上的访问权限。
同样地,Web 管理端从不显示任何机密。总览页只说"鉴权:已启用",不显示令牌值。 「生成凭据」也不破这条:生成的值只在那一次响应里出现,服务端不留副本。
唯一放开的那一项:能装 / 卸 IPClick 自己声明的五个可选组件。这条线仍然是划着
的——包名走白名单常量、命令用列表交给 subprocess、绑定当前解释器,做不到"执行
任意命令"。判断依据是:改超时、加节点、装一个自己声明过的可选依赖,这类操作改错了
也只是性能或可用性问题,可逆、可见;而关掉 SSRF 拦截、改掉令牌是不可逆的暴露。
| 措施 | 说明 |
|---|---|
默认只监听 127.0.0.1
|
见上文 |
| 无默认口令 | 不设就随机生成并打印 |
| CSRF 令牌 | 所有 POST 都校验 |
HttpOnly + SameSite=Strict 会话 cookie |
防 XSS 窃取与跨站提交 |
CSP default-src 'none'
|
页面不加载任何外部资源 |
CSP script-src 用脚本哈希
|
见下 |
connect-src 'self' |
页面里的 fetch 只能打本源 |
| 登录失败限速 | 防暴力破解 |
| 不显示机密 | 见上文 |
| 安全边界不可网页修改 | 见上文 |
页面里的 JS 是三百多行两段常量,只做六件没它不行的事:
- 手动切主题(记在
localStorage) - 弹窗确认
- 复制到剪贴板
- 就地 POST:「测试连接」「装 / 卸依赖」
- 按看的人的浏览器时区渲染时间 —— 服务端跑在 UTC 容器里时,东八区的人看到的仍是 自己的钟点。这件事只能在浏览器里做
- 实时刷新与档位切换
除此之外一行不多写。
前端仍然没有构建链路:没有模板引擎、框架、打包工具,也没有任何外部资源。 布局是纯 CSS Grid,脚本是源码里的两段常量。
CSP 里 script-src 用的是那两段脚本的 sha256 哈希,不是 'unsafe-inline'。
差别是实打实的:万一某处转义漏了、注入进一行 <script>,哈希对不上就执行不了;
而 'unsafe-inline' 会把这层保护整个让开。
style-src 仍然是 'unsafe-inline'——页面里有大量 style="width:37%" 这类属性
(分布条、趋势图的宽度),哈希覆盖不了行内样式属性,而样式注入的危害远小于脚本注入。
页面里也没有任何内联事件属性(onclick= 之类),全部用 addEventListener +
data-* 绑定——用事件属性就得在 CSP 里开 'unsafe-hashes',等于把口子重新开一条。
返回 JSON,都需要同一个会话 cookie——不是免鉴权的开放接口。
| 接口 | 用途 |
|---|---|
/api/status |
完整运行状态快照 |
/api/trace |
链路记录查询(支持与页面相同的过滤参数) |
/api/components/status |
当前安装任务的状态与输出 |
/api/components/action |
装 / 卸 / 下载浏览器本体(POST + CSRF) |
/api/nodes/probe |
探测某个节点(POST + CSRF) |
/action |
页面上的写操作总入口,如总览页的节点摘除 / 恢复(POST + CSRF) |
/skill.md |
下载 SKILL.md 原文 |
/deploy、/deploy.zip
|
子节点部署材料:生成 toml / .env / 启动命令,或打成 ZIP |
另有两个返回 HTML 片段的内部接口,供页面局部刷新用,不建议外部依赖:
/fragment/dashboard、/fragment/trace。
curl -c c.txt -d "username=admin&password=..." http://127.0.0.1:9527/login
curl -b c.txt http://127.0.0.1:9527/api/trace?limit=50Web 管理端 [WEB]
|
集群状态页 | |
|---|---|---|
| 怎么起 |
ipclick run -w,配置在 [WEB]
|
代码里 StatusPageServer(...).start(),没有配置节
|
| 默认端口 | 9527 | 9529 |
| 登录 | 有 | 无 |
| 能改东西 | 能(白名单内) | 不能,纯只读 |
| 内容 | 全局状态、请求流、试一试、组件、配置、节点 | 只有集群节点与健康状态 |
两个都默认只监听本机。