Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

宣告Gloria项目的死亡 #41

Open
3 of 5 tasks
BlackGlory opened this issue Nov 20, 2023 · 11 comments
Open
3 of 5 tasks

宣告Gloria项目的死亡 #41

BlackGlory opened this issue Nov 20, 2023 · 11 comments

Comments

@BlackGlory
Copy link
Owner

BlackGlory commented Nov 20, 2023

失败的Manifest V3迁移之旅

Gloria是建立在Manifest V2之上的浏览器扩展程序, Manifest V2现已被Manifest V3替代, 最终会失去浏览器支持, 详见Chrome的Manifest V2支持时间表.
在尝试将Gloria从Manifest V2迁移至Manifest V3的过程中, 我们遇到了无法克服的障碍, 这导致迁移无法完成, 项目因此走向终结.

Offscreen Documents + Web Workers

Manifest V3的Service Worker限制了执行动态代码的能力, 因此我们需要通过offscreen document来绕过限制.

在offscreen document里, 存在一个奇妙的例外允许执行动态代码, 尚不确定这是否属于安全漏洞.

借助这一例外, 仍然不足以运行预期中的Gloria脚本, 因为Worker无法导入外部模块(原本使用内置模块gloria-utils的做法因为无法实现对依赖项的版本控制, 遭到废弃).

尝试1:

const script = `import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

尝试2:

const script = `import('https://esm.sh/@blackglory/wait-for@0.7.4')`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

Refused to create a worker from 'https://esm.sh/@blackglory/wait-for@0.7.4' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'worker-src' was not explicitly set, so 'script-src' is used as a fallback.

尝试3:

import { javascript } from 'extra-tags'

const script = esm(`import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}
Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/9fa8785b-5e3f-42fd-86df-7b845f443070' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

loadESMScript @ 321e052c-f1af-4bad-9d77-41d771f9e83e:6
321e052c-f1af-4bad-9d77-41d771f9e83e:6 Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/9fa8785b-5e3f-42fd-86df-7b845f443070' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

尝试4:

import { javascript } from 'extra-tags'

const script = esm(`import('https://esm.sh/@blackglory/wait-for@0.7.4')`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}
Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/e13f475e-4dbd-4dec-811e-cd417d0a37b5' because it violates the following Content Security Policy directive: "script-src 'self'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

loadESMScript @ 1b0b39d4-517b-42b2-af32-be06cf3be884:6
1b0b39d4-517b-42b2-af32-be06cf3be884:6 Refused to load the script 'blob:chrome-extension://hjbedkekcmmaclhccpicpjbkbhjniblj/e13f475e-4dbd-4dec-811e-cd417d0a37b5' because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval' 'inline-speculation-rules' http://localhost:* http://127.0.0.1:*". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback.

实测表明针对性修改CSP也没用.

尽管我们确实可以在Worker里正常访问外部资源:

fetch('https://blackglory.me')
  .then(res => res.text())
  .then(console.log)

但这只能满足最低限度的Gloria脚本用例.
例如, 你可能会需要JSDOM, 因为你需要DOMParser来解析HTML或XML(原生Web Workers环境里并不存在DOMParser).

总之, 直接在offscreen document里执行动态代码的做法并不怎么靠谱:

  • 导入外部模块的能力受到限制, 无法创建具有外部依赖项的脚本.
  • 相关"特性"处于灰色地带, 随时有可能被"修复", 或者利用相关"特性"的扩展程序会被阻止上架Chrome Web Store.
    特别值得一提的是, Chrome官方认可的用户脚本现在要求扩展程序的用户手动启用开发者模式, 因此不授权就执行用户脚本的做法很可能是违规的.

Offscreen Documents + Iframe + Web Workers

Manifest V3实际上也有正规的执行不安全代码的方法, 即从Manifest V2就有的基于iframe的沙盒.
对于Gloria的用例, 需要在offscreen document里创建和使用基于iframe的沙盒.

最初, 我对此方案很有信心, 毕竟官方已经给出了执行不安全代码的方法, 还能出什么错呢?

尝试1:

fetch('https://blackglory.me')
  .then(res => res.text())
  .then(console.log)
Access to fetch at 'https://blackglory.me/' from origin 'null' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

原因在于iframe的origin是null, 因此不具有扩展程序的跨域能力, 在manifest.json里声明的host_permissions对iframe来说没有任何意义.
理论上, 可以通过为iframe启用allow-same-origin来使其获得与扩展程序相同的origin, 从而获得跨域能力.

尝试2:

const script = `import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to cross-origin redirects of the top-level worker script.

尝试3:

const script = `import('https://esm.sh/@blackglory/wait-for@0.7.4')`
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url, { type: 'module' })
Refused to cross-origin redirects of the top-level worker script.

尝试4:

import { javascript } from 'extra-tags'

const script = esm(`import { waitForTimeout } from 'https://esm.sh/@blackglory/wait-for@0.7.4'`)
const blob = new Blob([script], { type: 'application/javascript' })
const url = URL.createObjectURL(blob)
new Worker(url)

function esm(code) {
  return javascript`
    loadESMScript(${code})

    async function loadESMScript(script) {
      const blob = new Blob([script], { type: 'application/javascript' })
      const url = URL.createObjectURL(blob)
      await import(url)
      URL.revokeObjectURL(url)
    }
  `
}

正常运行, 至少我们有一种方式可以导入带有CORS header的外部模块.

尝试在manifest.json里添加allow-same-origin来解决跨域问题:

"content_security_policy": {
  "sandbox": "sandbox allow-scripts allow-same-origin;"
}
Invalid value for 'content_security_policy.sandbox'.

在HTML的iframe的sandbox属性上添加allow-same-origin则会静默失败.

显然, Chrome有意阻止为Sandbox启用allow-same-origin选项.
其中一个原因可能是同时启用allow-scriptsallow-same-origin能让沙盒内的代码逃逸.

至此我们陷入一个奇怪的局面:

  • 在offscreen document里不能导入外部模块, 但能访问任意外部资源.
  • 在iframe里不能访问任意外部资源, 但能导入外部模块(尽管是CORS限定, 但也够用了).

一种解决方案是在offscreen document里向iframe暴露一个API, 使其能够访问任意外部资源.
这意味着对fetch, EventSource, WebSocket这样的Web API进行包装.
此方案的实施难度大, 兼容性差, 其中一些数据类型很可能无法在上下文之间复制或转移, 不可行.

另一种解决方案是通过Manifest V3臭名昭著的DNR为响应添加CORS header, 从而绕过跨域限制.
然而, DNR的过滤条件无法匹配到由扩展程序沙盒发出的来自opaque origin的请求.

理想状态下, 这应该适用于沙盒, 可惜它没有:

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1]
, addRules: [
    {
      id: 1
    , condition: {
        initiatorDomains: [chrome.runtime.id]
      }
    , action: {
        type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS
      , responseHeaders: [
          {
            operation: chrome.declarativeNetRequest.HeaderOperation.SET
          , header: 'Access-Control-Allow-Origin'
          , value: '*'
          }
        ]
      }
    }
  ]
})

这适用于沙盒, 但影响了浏览器内的所有请求, 引入巨大的安全问题:

chrome.declarativeNetRequest.updateDynamicRules({
  removeRuleIds: [1]
, addRules: [
    {
      id: 1
    , condition: {}
    , action: {
        type: chrome.declarativeNetRequest.RuleActionType.MODIFY_HEADERS
      , responseHeaders: [
          {
            operation: chrome.declarativeNetRequest.HeaderOperation.SET
          , header: 'Access-Control-Allow-Origin'
          , value: '*'
          }
        ]
      }
    }
  ]
})

另一方面, 很难用DNR维持Gloria现有的Cookie, Referer, Origin动态注入能力, 这破坏了Gloria订阅私人消息的用例.

对Gloria的事后验尸

我在Gloria上的大多数技术决策都受到开发Gloria时的时代局限, 在这方面我不认为有做错什么.
在开发Gloria时, JavaScript被CoffeeScript替代, 因此我选择用CoffeeScript的超集LiveScript来开发, ESM支持和ESM CDN不存在, 流行模块标准至少有三个, 大多数MVVM都被Angular带上了双向数据绑定的弯路, TypeScript则根本没几个人使用.
如今JavaScript已经发展到ES2023, 我们有了原生的ESM支持, 有像https://esm.sh这样的ESM CDN,
有React这样成熟的MVVM框架, 并且大多数仍被使用的npm模块要么用TypeScript重写, 要么具有TypeScript类型定义.

现有的Web技术是当年难以想象的, Gloria项目最失败的部分是没有跟上Web技术的步伐,
这都是因为我在开发Gloria时没有采用一个易于维护的架构.
当Gloria的代码逐渐变得陈旧, 任何大的改变都需要以重写的形式来实现时, 项目的发展理所当然地停滞了.
最终, 重写没有到来, 到来的是Manifest V3替代Manifest V2的历史车轮.

这是Gloria原本预定实装的新脚本格式, 对想要开发类似项目的开发者也许会有参考价值:

// -- 此脚本的各种元数据, 语法类似于油猴脚本 --
// @name 脚本显示的名称
// @update-url 脚本的更新地址

// -- 导入外部ESM模块 --

// -- 其他只在创建Worker时运行一次的代码 --

// -- 作为ESM模块的默认项返回, 执行器将会根据返回值类型决定是否采用轮询方式 --
export default function (signal: AbortSignal):
| INotification[]
| PromiseLike<INotification[]>
| Observable<INotification>
| AsyncIterable<INotification>

接下来会发生什么?

  • 随着Manifest V2的生命周期走向终结, 你无法在Chromium浏览器上继续下载、安装、使用Gloria.
    你可以在旧版本的Chromium里继续使用Gloria, 但这注定无法长期维持下去.
    作为用户, 你可以尝试转去使用Gloria的开源替代项目Gloria-X,
    Gloria-X很可能最终会面临与本项目类似的问题, 但也许相关功能可以在Firefox上延续下去.
  • 该项目的源代码存储库会转入归档状态, 仅提供代码下载功能, 直到未来某一天我决定删除它.
    作为开发者, 你可以转去为Gloria的开源替代项目Gloria-X做贡献.
  • https://gloria.pub网站将会下线, 服务器源代码将被删除, 数据库将被删除, 域名将停止续费.
  • 该项目依赖项的源代码存储库, 例如worker-sandboxgloria-sandbox将被删除, 你仍然可以在npm里下载这些依赖项.
    如果你需要在Web Workers里动态定制沙盒环境, 我相信delight-rpc/browser是更好的解决方案.
  • 我会转去尝试开发在浏览器环境以外运行的替代解决方案.
    脱离浏览器环境的解决方案注定不会有Gloria这样的集成度, 我相信它不会适用于绝大多数现有的Gloria用户.

事情还会有转机吗?

一旦我开始开发替代解决方案, 就不再可能会有转机, 因为我不能同时维护复数服务于相似目的的项目.

@BlackGlory BlackGlory pinned this issue Nov 20, 2023
@arpir
Copy link

arpir commented Nov 20, 2023

真是个悲伤的故事,好在我一直在用绿色版的Edge,也通过 https://gloria.pub/ 网站,学到了一些基础编写小任务的知识,就算以后脱离了网站自己也可以编写一些小任务,即使在这个项目的生命周期结束之前,后来的人也能通过 Wayback Machine 项目来查看网页的快照

最后感谢项目的开发者,向你致敬😘

@BlackGlory

This comment was marked as outdated.

@BlackGlory

This comment was marked as outdated.

@BlackGlory
Copy link
Owner Author

今天才发现不知道哪个版本的Chrome又把这个flag加回来了, 光在我记忆里这个flag已经被删除过两次.

image

image

由于我非常嫌弃Windows系统的通知系统, 我还用Electron重新实现过Chrome风格的通知.
如果Chrome能继续把这个flag保留下去, 那么开发专门用于弹出Chrome风格通知的客户端就不必要了, 只要做一个PWA就可以通杀所有支持Chrome的平台.

甚至有两个现成的项目可以改:

@362227
Copy link

362227 commented Dec 8, 2023

chrome插件别下架,我还在用

@BlackGlory
Copy link
Owner Author

扩展不会主动下架的.
如果未来发现扩展的商店页打不开了, 那也是CWS把它判定为已淘汰的扩展类型, 将其隐藏掉了.
直到这个项目彻底死亡我估计还有1年的时间.

@LightAPIs
Copy link

今天才发现不知道哪个版本的Chrome又把这个flag加回来了, 光在我记忆里这个flag已经被删除过两次.

😝 Chrome 里的 flag 确实经常回旋镖。

@BlackGlory

This comment was marked as outdated.

@BlackGlory

This comment was marked as outdated.

@BlackGlory
Copy link
Owner Author

原定的Gloria继任项目Hallu现已被Deno包extra-deduplicator替代, 展开上方被标记为过时的评论可以找到这么做的原因.

extra-deduplicator项目是直接在Hallu项目的基础上重写的,
ce96a1a21471fcd703c74b17dc829240364852c4为Hallu的最后一次提交,
之后的提交为extra-deduplicator的代码.

提供类Chrome通知的跨平台桌面应用程序notifier现在也已基本可用, 它基本上使用与Gloria相同的通知结构.

@BlackGlory
Copy link
Owner Author

BlackGlory commented Jun 2, 2024

gloria.pub网站现已下线, 域名将于1个月后到期, 期间访问网站将跳转至本页.

这是网站所有任务脚本的数据库dump: gloria.pub scripts.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants