当前域名暂时无法从网络访问。请通过其他方式联系网站管理员获取最新访问入口。
- - -` - -// EffectiveSWOfflineHTML returns custom HTML when set, otherwise the built-in default. -func EffectiveSWOfflineHTML(cfg ConfigSnapshot) string { - if strings.TrimSpace(cfg.SWOfflineHTML) == "" { - return DefaultSWOfflineHTML - } - return cfg.SWOfflineHTML -} - -// ServiceWorkerSupportFiles returns the sw.js script and offline contact page. -func ServiceWorkerSupportFiles(cfg ConfigSnapshot) []SupportFile { - if !cfg.SWOfflineEnabled { - return nil - } - return []SupportFile{ - {Path: swDirPrefix + "sw.js", Content: defaultSWJS()}, - {Path: swDirPrefix + "offline.html", Content: EffectiveSWOfflineHTML(cfg)}, - } -} - -func defaultSWJS() string { - return `var CACHE = "openflare-offline-v1"; -var OFFLINE = "/offline.html"; -self.addEventListener("install", function (e) { - e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll([OFFLINE]); })); - self.skipWaiting(); -}); -self.addEventListener("activate", function (e) { - e.waitUntil(caches.keys().then(function (keys) { - return Promise.all(keys.filter(function (k) { return k !== CACHE; }).map(function (k) { return caches.delete(k); })); - })); - self.clients.claim(); -}); -self.addEventListener("fetch", function (e) { - if (e.request.method !== "GET") { return; } - e.respondWith( - fetch(e.request).catch(function () { - return caches.match(e.request).then(function (r) { return r || caches.match(OFFLINE); }); - }) - ); -}); -` -} - -// renderServiceWorkerChallenger emits SW static locations and the homepage -// challenge intercept for HTTPS server blocks. -func renderServiceWorkerChallenger(cfg ConfigSnapshot) string { - if !cfg.SWOfflineEnabled { - return "" - } - var builder strings.Builder - builder.WriteString("\n location = /sw.js {\n") - builder.WriteString(" alias " + SWDirPlaceholder + "/sw.js;\n") - builder.WriteString(" default_type application/javascript;\n") - builder.WriteString(" add_header Service-Worker-Allowed /;\n") - builder.WriteString(" add_header Cache-Control \"no-cache\";\n") - builder.WriteString(" }\n\n") - builder.WriteString(" location = /offline.html {\n") - builder.WriteString(" alias " + SWDirPlaceholder + "/offline.html;\n") - builder.WriteString(" default_type text/html;\n") - builder.WriteString(" add_header Cache-Control \"no-cache\";\n") - builder.WriteString(" }\n\n") - builder.WriteString(" location = /__openflare_sw_challenge {\n") - builder.WriteString(" internal;\n") - builder.WriteString(" content_by_lua_file " + SWDirPlaceholder + "/challenge.lua;\n") - builder.WriteString(" }\n") - return builder.String() -} -``` - -- [ ] **Step 4: 接入 `Render` 追加 support files** - -在 `render.go` `Render` 函数内、`originErrorPageSupportFile` 追加之后追加: - -```go - if doc.OpenRestyConfig.SWOfflineEnabled { - files = append(files, ServiceWorkerSupportFiles(doc.OpenRestyConfig)...) - } -``` - -- [ ] **Step 5: 接入 `RenderRouteConfig` 注入挑战** - -在 `RenderRouteConfig` 内 `renderProxyRoute` / `renderPagesRoute` 调用之前,将 SW 拦截接入 server 块。将 `renderAccessBlock(siteName, powEnabled)` 调用处扩展:新建 `renderServerAccess(siteName, powEnabled, cfg)` 封装,并在其中追加 SW 运行时检查。具体为在 `renderAccessBlock` 生成的 access 块内,追加对 `sw.runtime` 的调用。 - -简化实现:新增 `renderAccessBlockWithSW(siteName string, powEnabled bool, cfg ConfigSnapshot) string`,返回 `renderAccessBlock(siteName, powEnabled)` 与(当 `SWOfflineEnabled` 时)追加: - -``` - access_by_lua_block { - if not string.find(package.path, "__OPENFLARE_LUA_DIR__/?.lua", 1, true) then - package.path = "__OPENFLARE_LUA_DIR__/?.lua;__OPENFLARE_LUA_DIR__/?/init.lua;" .. package.path - end - require("sw.runtime").check() - } -``` - -然后将 `renderHTTPProxyServer`、`renderHTTPSServer`、`renderHTTPPagesServer`、`renderHTTPSPagesServer` 中 `renderAccessBlock(...)` 替换为 `renderAccessBlockWithSW(..., cfg)`,并在各自 server 块内追加 `renderServiceWorkerChallenger(cfg)` 输出。 - -**注意:** `renderAccessBlock` 在既有 powEnabled 分支已含 `access_by_lua_block`。为兼容,`renderAccessBlockWithSW` 在 powEnabled 分支内合并 SW check 到同一块;非 pow 分支额外追加一个块。本步以**仅新增 server 级 SW location + 独立 `access_by_lua_block`** 为最小实现;若 nginx 同 server 存在两个 `access_by_lua_block`,运行时只执行最后一个——**故实现必须合并**。请在实现时确认 `renderAccessBlock` 各分支,将 SW check 合并进唯一 access 块内,避免覆盖 WAF/PoW。 - -- [ ] **Step 6: 运行测试** - -Run: `go test ./pkg/render/openresty/...` -Expected: PASS - -- [ ] **Step 7: 提交** - -```bash -git add pkg/render/openresty/service_worker.go pkg/render/openresty/service_worker_test.go pkg/render/openresty/render.go -git commit -m "feat(openresty): render sw offline assets and challenge intercept" -``` - ---- - -### Task 5: config_version snapshot 接入全局 Option - -**Files:** -- Modify: `internal/apps/openflare/config_version/snapshot.go:143-147`(`openRestyConfigSnapshot` 字段) -- Modify: `internal/apps/openflare/config_version/snapshot.go:559-563`(`buildOpenRestyConfigSnapshot` 读取) -- Modify: `internal/apps/openflare/config_version/logics.go:537-541`(diff 追加) -- Modify: `internal/apps/openflare/config_version/logics.go:604-608`(option keys 追加) - -**Interfaces:** -- Consumes: `model.ConfigKeySWOfflineEnabled` / `.SWOfflineHTML`。 -- Produces: snapshot JSON 内 `sw_offline_enabled` / `sw_offline_html` 字段,触发 checksum 变化。 - -- [ ] **Step 1: snapshot 结构体追加字段** - -在 `openRestyConfigSnapshot`(`snapshot.go:143-147`,`OriginErrorPageGetOnly` 后)追加: - -```go - SWOfflineEnabled bool `json:"sw_offline_enabled,omitempty"` - SWOfflineHTML string `json:"sw_offline_html,omitempty"` -``` - -- [ ] **Step 2: build 读取配置** - -在 `buildOpenRestyConfigSnapshot`(`snapshot.go:559-563`,`OriginErrorPageGetOnly` 赋值后)追加: - -```go - SWOfflineEnabled: getBoolConfig(model.ConfigKeySWOfflineEnabled, false), - SWOfflineHTML: getStringConfig(model.ConfigKeySWOfflineHTML, ""), -``` - -- [ ] **Step 3: diff 追加** - -在 `diffOpenRestyOptionDetails`(`logics.go:540` 后)追加: - -```go - appendIfChanged("SWOfflineEnabled", fmt.Sprintf("%t", left.SWOfflineEnabled), fmt.Sprintf("%t", right.SWOfflineEnabled)) - appendIfChanged("SWOfflineHTML", left.SWOfflineHTML, right.SWOfflineHTML) -``` - -- [ ] **Step 4: option keys 追加** - -在 `openRestyOptionKeys()`(`logics.go:607` 后)追加: - -```go - "SWOfflineEnabled", - "SWOfflineHTML", -``` - -- [ ] **Step 5: 运行测试** - -Run: `go test ./internal/apps/openflare/config_version/...` -Expected: PASS - -- [ ] **Step 6: 提交** - -```bash -git add internal/apps/openflare/config_version/snapshot.go internal/apps/openflare/config_version/logics.go -git commit -m "feat(config): wire sw offline options into config snapshot" -``` - ---- - -### Task 6: Agent 侧 SW Lua 资源与占位符替换 - -**Files:** -- Create: `internal/apps/agent/nginx/sw_assets.go` -- Modify: `internal/apps/agent/nginx/manager.go:393-410`(`EnsureLuaAssets` 追加 SW Lua) -- Modify: `internal/apps/agent/nginx/manager.go:526-528`(checksum 归一化 SW 路径) -- Modify: `internal/apps/agent/nginx/manager.go:1381-1383`(renderRouteConfig 替换 SW 占位符) - -**Interfaces:** -- Consumes: `openrestyrender.SWDirPlaceholder`、`openrestyrender.SWChallengeLua`、`openrestyrender.SWRuntimeLua`。 -- Produces: `ManagedSWLuaFiles() []protocol.SupportFile`(`sw/runtime.lua`、`sw/challenge.lua`)。 - -- [ ] **Step 1: 创建 `sw_assets.go`** - -```go -package nginx - -import ( - "github.com/Rain-kl/Wavelet/internal/apps/agent/protocol" -) - -const openRestySWRuntimeLua = `local source = debug.getinfo(1, "S").source or "" -if string.sub(source, 1, 1) == "@" then - local script_path = string.sub(source, 2) - local base_dir = string.match(script_path, "^(.*)/sw/[^/]+%.lua$") - if base_dir and base_dir ~= "" and not string.find(package.path, base_dir, 1, true) then - package.path = base_dir .. "/?.lua;" .. base_dir .. "/?/init.lua;" .. package.path - end -end - -local function is_real_browser(ua) - if not ua or ua == "" then return false end - -- Chrome/Edge/CentOS-style: "Chrome/120" - if string.find(ua, "Chrome/%d", 1, true) then return true end - -- Firefox: "Firefox/120" - if string.find(ua, "Firefox/%d", 1, true) then return true end - -- Safari (non-Chrome, e.g. "Version/17.0 Safari") - if not string.find(ua, "Chrome", 1, true) and string.find(ua, "Safari", 1, true) then return true end - return false -end - -local function pass_through() - return true -end - -function _M_check() - local ua = ngx.var.http_user_agent or "" - if not is_real_browser(ua) then return pass_through() end - - local uri = ngx.var.uri or "" - if uri ~= "/" then return pass_through() end - - local cookie = ngx.var["cookie___openflare_sw"] - if cookie and cookie ~= "" then return pass_through() end - - -- intercept: internal redirect to challenge page, which registers SW + sets cookie - local redir = ngx.var.scheme .. "://" .. ngx.var.host .. uri .. (ngx.var.args and ("?" .. ngx.var.args) or "") - ngx.req.set_uri_args({ redir = redir }) - return ngx.exec("/__openflare_sw_challenge") -end -` - -const openRestySWChallengeLua = `local args = ngx.req.get_uri_args() -local redir = args["redir"] or "/" -ngx.header["Set-Cookie"] = "__openflare_sw=1; Path=/; Max-Age=31536000" -ngx.header.content_type = "text/html; charset=utf-8" -ngx.say([[ - - - - - -- 仅对 HTTPS 网站生效;未启用的站点不受影响。 -
-- 留空则使用内置默认模板。 -
-- 仅对选中的 HTTPS 域名生效;留空则不注入。 -
-- {fields.enabled ? '尚未选择生效域名,保存后不注入任何站点。' : '启用离线兜底后可选择生效域名。'} -
- ) : ( -