竹林里有冰的博客

竹林里有冰的博客

Vercel 的缓存控制,你注意过吗?

Vercel 默认的缓存配置其实并不合理,但鲜有人注意。 先看效果 分析 测试方案 这两张图都是我博客在 PageSpeed Insights 上测得的,测试步骤如下: 部署 在 PageSpeed Insights 进行第一次测试 等待 120s,防止 PageSpeed Insights 拿之前的结果糊弄你 进行第二次测试,取第二次测试的结果 取第二次结果的目的是为了让 PageSpeed Insights 所命中的 Vercel CDN 节点完成回源,并将内容缓存在 CDN 节点上,这样第二次访问的时候就会直接从 CDN 的缓存中得到结果,不需要回源。 那我们看图一的测试结果,正常吗?针对首页的单个 html 加载时长达到了 450ms,看着不算慢,但其实细究下来是有问题的。 Vercel 采用的是 Amazon 提供的全球 CDN 网络,在我们的首次访问之后,CDN 节点应当该已经缓存了首页的内容,第二次访问的时候应该是直接从 CDN 节点的缓存中获取内容。 合理的时长是多久呢? TCP 建立连接的三次握手,需要 1.5 个往返时延(RTT),再加上 TLS 1.3 握手的 1 个 RTT,共计 2.5 个 RTT。 HTML 文件大小 18KB,初始拥塞窗口(IW)10 MSS ≈ 1460 字节 ≈ 14.6KB,理论上应该可以在两个 RTT 内传输完毕。 共计 4.5 个 RTT。 PageSpeed Insights 测试时使用的节点大概率是在美国,Amazon CDN 在美国的节点覆盖非常广泛,单个 RTT 时长控制在 5ms 以内绰绰有余,所以理论加载时长应该在 22.5ms 左右。加上 DNS 解析时长(这个也不多,因为两分钟前有过一次访问,这次不是冷启动)和一些不可控的网络抖动,50ms 以内应该是完全没有问题的。 但实际测得的时长却高达 450ms,差了近 9 倍,这就很不合理了。 我们再来看图二的结果,单个 HTML 加载时长降到了 41ms,完全符合预期。 为什么会有这么大的差异呢?原因就在于 Vercel 对缓存控制的设置上。 Vercel 的缓存控制 在 Vercel 上部署的网站,默认情况下,Vercel 会对 HTML 文件设置如下的缓存控制头: cache-control: public, max-age=0, must-revalidate 这个设置的含义是: public:响应可以被任何缓存区缓存,包括浏览器和 CDN。 max-age=0: 响应的最大缓存时间为 0 秒,意味着响应一旦被缓存后立即过期。 must-revalidate:一旦响应过期,缓存必须向源服务器验证其有效性。 结合这三个指令,Vercel 实际上是告诉 CDN 节点:你可以缓存这个 HTML 文件,但每次在使用缓存之前都必须回源验证其有效性。由于 max-age=0,缓存一旦存储就立即过期,因此每次请求都会触发回源验证。 尽管 HTTP/1.1 和 HTTP/2 中的缓存验证通常使用条件请求(如文件的 ETag 或 Last-Modified 头)来节省传输流量,但这仍然需要与源服务器进行往返通信,从而增加了额外的延迟开销。 所以,在 Vercel 默认配置下,任何请求的响应都不会被 CDN 节点直接缓存,大致的流程如下: sequenceDiagram participant Client participant CDN participant Vercel Client->>CDN: 请求 HTML 文件 CDN->>Vercel: 条件请求验证缓存 Vercel->>CDN: 返回最新的 HTML 文件或 304 Not Modified CDN->>Client: 返回 HTML 文件 解决方案 要解决这个问题,我们需要调整 Vercel 上的缓存控制设置,使得 HTML 文件能够被 CDN 节点缓存一段时间,而不需要每次都回源验证。 Vercel 允许我们在项目的根目录下创建一个 vercel.json 文件以对 Vercel 的部署行为进行一系列的配置,其中就包括 HTTP 的响应头的配置。 我的博客采用 Nuxt.js 框架构建,生成的构建产物大概分为两类: HTML 文件:这些文件的内容可能会频繁变化,不能设置过长的缓存时间; 静态资源文件:包括 JavaScript、CSS 等,这些文件的文件名通常带有 hash 值,可以设置较长的缓存时间甚至被标记为不可变(immutable)。 在部署流程上,我的博客在每次推送后会先 Github Actions 中构建成静态页面,再部署到 Vercel 上,所以我在我项目的 public 目录下创建了 vercel.json 文件(这样 vercel.json 文件就会在构建产物的根目录),内容如下: { "headers": [ { "source": "/(.*)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=0, s-maxage=600, must-revalidate" } ] }, { "source": "/(.*)\\.(css|js)", "headers": [ { "key": "Cache-Control", "value": "public, max-age=31536000, immutable" } ] } ] } 这里我对所有的 CSS 和 JS 文件设置了 max-age=31536000, immutable,这样这些静态资源文件就可以被浏览器和 CDN 长时间缓存。而对于所有其他文件(主要是 HTML 文件),我设置了 max-age=0, s-maxage=600, must-revalidate,这样 HTML 文件就可以被 CDN 缓存 10 分钟,在这 10 分钟内的请求都可以直接从 CDN 节点的缓存中获取内容,而不需要回源验证。 这样一来,经过修改后的缓存控制设置,HTML 文件的请求流程变成了: sequenceDiagram participant Client participant CDN Client->>CDN: 请求 HTML 文件 CDN->>Client: 直接返回缓存的 HTML 文件 从而大大减少了请求的延迟,提高了页面加载速度。 其他 Vercel 所采用的架构并不是传统的 「源站 - CDN」 架构,而是更接近 「全球多区域存储 + CDN边缘缓存」 的架构,所以即使是回源请求,Vercel 也会尽量从离用户最近的存储节点获取内容,从而减少延迟。但这并不意味着回源请求的延迟可以忽略不计,尤其是在追求极致的加载速度时,合理的缓存控制仍然是非常重要的。 参见 Cache-Control headers | Vercel Vercel CDN Cache | Vercel HTTP caching - HTTP | MDN

2025/12/23
articleCard.readMore

小记 —— Caddy 在 Layer 4 上的流量代理实践

背景 在我的一台优化线路 vps 上,我的 443 端口要承担两个职责 作为我博客对中国大陆境内访客的服务提供者,同时承担 https 流量加解密和 static server 的职责 把某些特殊用途的流量特征通过一些手段伪装成某些知名、常见、且被广泛允许的站点的 https 流量 (没错,是 Reality) 因此,我需要一个能够在同一台服务器的同一端口上同时处理这两种职责的方案。 方案选择 其实我很早就知道 Nginx 的 stream 关键词可以实现 Layer 4 (即 TCP 字节流原样转发)下基于 SNI 识别实现的分流功能,但我其实一直是 Caddy 的忠实用户,写了不少 Caddy 相关的博文。因此,尽管 Nginx 在不久前已经支持了 ACME v2,但 Caddyfile 的简洁和易用性依然让我更倾向于使用 Caddy 来实现这个功能。 经过一番查阅,Caddy 最新版本(v2.10)并不支持 Layer 4 的流量代理功能,但有一个名为 caddy-l4 的社区模块可以实现这个功能,在 Github 上有 1.5k stars,且最近也有更新维护,于是我决定尝试使用这个模块来实现我的需求。 安装 尽管 Caddy 官方提供的 APT 源中的 Caddy 版本并不包含 caddy-l4 模块,但我仍然建议先通过 APT 安装 Caddy 的基础版本,然后再通过 Caddy 官方提供的在线构建页面选择需要的模块来生成自定义的 Caddy 二进制文件,下载后替换掉系统中的 Caddy 可执行文件。这样做的好处是可以方便地完成 systemd 服务的配置。但注意关闭 Caddy 的 APT 源,以免后续自动更新覆盖掉自定义编译的版本。 后续更新可以通过 caddy upgrade 命令来完成,caddy 会自动列出当前二进制文件所包含的模块,并自动触发官网的在线构建来生成新的二进制文件并进行替换,只需手动重启 systemd 服务即可完成更新。 如果 Caddy 官方提供的在线构建失败(最近挺不稳定的),可以参考文档使用 xcaddy 在本地编译 Caddy: xcaddy build --with github.com/mholt/caddy-l4 配置 这是我先前博客站点的 Caddyfile 配置: zhul.in { root * /var/www/zhul.in encode zstd gzip file_server handle_errors { rewrite * /404.html file_server } } www.zhul.in { redir https://zhul.in{uri} } zhul.in 和 www.zhul.in 都占用了 80 和 443 端口,因此需要把这两个站点的 443 端口的监听改到其他端口,把 443 端口交给 caddy-l4 来处理。 修改后的 Caddyfile 如下: http://zhul.in:80, https://zhul.in:8443 { root * /var/www/zhul.in encode zstd gzip file_server handle_errors { rewrite * /404.html file_server } } http://www.zhul.in:80, https://www.zhul.in:8443 { redir https://zhul.in{uri} } 随后,添加 caddy-l4 的配置: { layer4 { :443 { @zhulin tls sni zhul.in www.zhul.in route @zhulin { proxy 127.0.0.1:8443 } @proxy tls sni osxapps.itunes.apple.com route @proxy { proxy 127.0.0.1:20443 } } } } 这里的写法还挺简单的,首先在 layer4 块中监听 443 端口,然后通过 @name tls sni domain 的方式定义基于 SNI 的匹配规则,随后通过 route @name 定义匹配到该规则时的处理方式,这里使用 proxy ip:port 来实现流量的转发。 由于我的妙妙流量伪装成了 Apple 的 itunes 流量,因此在上面的配置中的 SNI 特征是 osxapps.itunes.apple.com,这些流量会被转发到本地的 20443 端口,由另一个奇妙服务来处理。 caddy-l4 还提供了一些其他的匹配方式和处理方式,具体可以参考他们在 Github 中给到的 examples。 完成配置后,重启 Caddy 服务: sudo systemctl restart caddy 参见 mholt/caddy-l4: Layer 4 (TCP/UDP) app for Caddy Build from source — Caddy Documentation

2025/12/10
articleCard.readMore

你的域名后缀拖慢你的网站速度了嘛?——再谈 DNS 冷启动

在上一篇博客中,我提到过一个核心观点——对于流量少、访客的地理位置不集中的小型站点,DNS 冷启动不是偶发的“意外”,而是一种被动的“常态”。 对于大多数站长而言,自己的站点流量不是一时半刻就能提上去的,因此我们的访客大概率都要走完一遍完整的 DNS 解析过程。上一篇博客中我提到过更改为距离访客物理位置更近的权威 DNS 服务器来提升速度,但 TLD(域名后缀)的 Nameservers 是我们无法改变的,也就是下图中红色背景的那一段解析过程。 sequenceDiagram autonumber participant User as 用户/浏览器 participant Local as 本地DNS<br>递归解析器 participant Root as 根域名服务器 participant TLD as 顶级域服务器<br>(TLD Server) participant Auth as 权威DNS服务器 Note over User,Auth: DNS 递归查询完整流程 User->>Local: 查询域名 www.example.com Note over Local: 检查缓存 (MISS) Local->>Root: 查询 .com 的 TLD 服务器 Root-->>Local: 返回 .com TLD 服务器地址 %% --- 重点高亮区域开始 --- rect rgb(255, 235, 235) Note right of Local: ⚠️ 本文核心讨论区域 <br> (TLD 解析时延) Local->>TLD: 查询 example.com 的权威服务器 Note left of TLD: 这里的物理距离与 Anycast 能力<br>决定了是否存在数百毫秒的延迟 TLD-->>Local: 返回 example.com 的权威服务器地址 end %% --- 重点高亮区域结束 --- Local->>Auth: 查询 www.example.com 的 A 记录 Auth-->>Local: 返回 IP 地址 (e.g., 1.1.1.1) Note over Local: 缓存结果 (TTL) Local-->>User: 返回最终 IP 地址 User->>Auth: 建立 TCP 连接 / HTTP 请求 所以,如果你还没有购买域名,但想要像个 geeker 一样追求极致的首屏加载(哪怕你并没有多少访客),你该选择哪个 TLD 呢? 简单测试 一个简单的方法是,直接去 ping TLD 的 nameserver,看看访客所请求的公共 DNS 服务器在这一段解析中所花费的时常。 以我的域名 zhul.in 为例,在 Linux 下,可以通过 dig 命令拿到 in 这个 TLD 的 Nameserver dig NS in. 随后可以挑选任何一个 Nameserver(公共 DNS 服务器其实有一套基于历史性能的选择策略),直接去 ping 这个域名 我这里的网络环境是杭州移动,如果我在我的局域网开一台 DNS 递归服务器,这个结果就是在上面那张时序图中红色部分所需要时长的最小值(DNS 服务器还需要额外的时长去处理请求)。 借助一些网站提供的多个地点 ping 延迟测试,我们可以推测这个 TLD 在全球哪些国家或地区部署了 Anycast(泛播)节点,下图为 iplark.com 提供的结果。 可以推测,in 的 TLD Nameserver 起码在日本、香港、美国、加拿大、欧洲、澳大利亚、巴西、印度、南非等多地部署了 Anycast 节点,而在中国大陆境内的延迟较高。 作为对比,我们可以通过同样的方法再看看 cn 域名的 TLD Nameserver 的 Anycast 节点。 经过 itdog.cn 的测试,推测 cn 域名的 TLD Nameserver 可能仅在北京有节点。 更进一步的的实验方案 上面的测试方法只是一个简易的判断方法,在现实中会有很多的外部因素影响 DNS 冷启动的解析时长: 公共 DNS 服务器和 TLD Nameserver 之间存在 peer,他们的通信非常快 TLD Nameserver 的性能差,需要额外的几十 ms 去处理你的请求 TLD 的几个 Nameserver 有快慢之分,而你选用的公共 DNS 服务器能根据历史数据选择较快的那个 ... 所以,我们需要有一个基于真实的 DNS 解析请求的测试方案 对于 DNS 冷启动相关的测试一直以来存在一个困境——公共 DNS 服务器不归我们管,我们无法登陆上去手动清除它的缓存,因此所有的测试都只有第一次结果才可能有效,后续的请求会直接打到缓存上。但这一次我们测试的是公共 DNS 服务器到 TLD Nameserver 这一段的延迟,在 Gemini 的提醒下,我意识到可以在不同地区测试公共 DNS 对随机的、不存在的域名的解析时长,这能够反应不同 TLD 之间的差异。 所以,测试代码在下面,你可以使用常见的 Linux 使用 bash 执行这段代码,需要确保装有 dig 和 shasum 命令,并且推荐使用 screen / tmux 等工具挂在后台,因为整个测试过程可能会持续十几分钟。如果你所采用的网络环境在中国大陆境内,我建议你把代码中的公共 DNS 服务器换成 223.5.5.5 / 119.29.29.29 ,应该会更符合境内访客的使用环境。 #!/bin/bash # ================= 配置区域 ================= # CSV 文件名 OUTPUT_FILE="dns_benchmark_results.csv" # DNS 服务器 DNS_SERVER="8.8.8.8" # 待测试的 TLD 列表 # 包含:全球通用(com), 国别(cn, de), 热门技术(io, xyz), 以及可能较慢的后缀 TLDS_TO_TEST=("com" "net" "org" "cn" "in" "de" "cc" "site" "ai" "io" "xyz" "top") # 每个 TLD 测试次数 SAMPLES=1000 # 每次查询间隔 (秒),防止被 DNS 服务器判定为攻击 # 1000次 * 0.1s = 100秒/TLD,总耗时约 15-20 分钟 SLEEP_INTERVAL=0.1 # =========================================== # 初始化 CSV 文件头 echo "TLD,Domain,QueryTime_ms,Status,Timestamp" > "$OUTPUT_FILE" echo "=============================================" echo " DNS TLD Latency Benchmark Tool" echo " Target DNS: $DNS_SERVER" echo " Samples per TLD: $SAMPLES" echo " Output File: $OUTPUT_FILE" echo "=============================================" echo "" # 定义进度条函数 function show_progress { # 参数: $1=当前进度, $2=总数, $3=当前TLD, $4=当前平均耗时 let _progress=(${1}*100/${2}) let _done=(${_progress}*4)/10 let _left=40-$_done # 构建填充字符串 _fill=$(printf "%${_done}s") _empty=$(printf "%${_left}s") # \r 让光标回到行首,实现刷新效果 printf "\rProgress [${_fill// /#}${_empty// /-}] ${_progress}%% - Testing .${3} (Avg: ${4}ms) " } # 主循环 for tld in "${TLDS_TO_TEST[@]}"; do # 统计变量初始化 total_time_accum=0 valid_count=0 for (( i=1; i<=${SAMPLES}; i++ )); do # 1. 生成随机域名 (防止缓存命中) # 使用 date +%N (纳秒) 确保足够随机,兼容 Linux/macOS RAND_PART=$(date +%s%N | shasum | head -c 10) DOMAIN="test-${RAND_PART}.${tld}" TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S") # 2. 执行查询 # +tries=1 +time=2: 尝试1次,超时2秒,避免脚本卡死 result=$(dig @${DNS_SERVER} ${DOMAIN} A +noall +stats +time=2 +tries=1) # 提取时间 (Query time: 12 msec) query_time=$(echo "$result" | grep "Query time" | awk '{print $4}') # 提取状态 (status: NXDOMAIN, NOERROR, etc.) status=$(echo "$result" | grep "status:" | awk '{print $6}' | tr -d ',') # 3. 数据清洗与记录 if [[ -n "$query_time" && "$query_time" =~ ^[0-9]+$ ]]; then # 写入 CSV echo "${tld},${DOMAIN},${query_time},${status},${TIMESTAMP}" >> "$OUTPUT_FILE" # 更新统计 total_time_accum=$((total_time_accum + query_time)) valid_count=$((valid_count + 1)) current_avg=$((total_time_accum / valid_count)) else # 记录失败/超时 echo "${tld},${DOMAIN},-1,TIMEOUT,${TIMESTAMP}" >> "$OUTPUT_FILE" current_avg="N/A" fi # 4. 显示进度条 show_progress $i $SAMPLES $tld $current_avg sleep $SLEEP_INTERVAL done # 每个 TLD 完成后换行 echo "" echo "✅ Completed .${tld} | Final Avg: ${current_avg} ms" echo "---------------------------------------------" done echo "🎉 All Done! Results saved to $OUTPUT_FILE" 测试结果 免责声明:以下测试结果仅供参考,不构成任何购买推荐,且仅代表测试当日(2025.11.24)的网络情况,后续不会进行跟进。DNS 冷启动对于大型站点几乎没有影响,仅小站需要关注。本次测试中,所有境内检测点使用 223.5.5.5 作为 DNS 服务器,境外检测点使用 8.8.8.8。 测试点/延迟(ms) .com .net .org .cn .in .de .cc .site .ai .io .xyz .top 🇨🇳 上海腾讯云 438 429 470 30 535 353 476 454 367 485 444 43 🇨🇳 北京腾讯云 425 443 469 17 350 420 466 647 582 461 559 9 🇭🇰 香港 Yxvm 75 75 363 227 6 11 61 6 33 126 5 7 🇨🇳 彰化(台湾) Hinet 90 87 128 213 59 38 76 37 73 94 36 47 🇯🇵 大阪 Vmiss 20 19 244 309 15 24 17 35 19 65 37 90 🇸🇬 新加坡 Wap 6 9 139 398 6 10 7 17 7 110 17 66 🇺🇸 洛杉矶 ColoCrossing 7 7 307 137 4 64 5 62 5 49 47 231 🇩🇪 杜塞尔多夫 WIIT AG 16 17 288 82 75 15 14 24 66 73 24 306 🇦🇺 悉尼 Oracle 33 31 12 338 7 13 121 7 10 9 7 191 通过上面的数据,我们可以看到 .cn 和 .top 是所有测试的域名后缀中在中国大陆境内解析速度最快的,但选择 .cn 和 .top 意味着你需要牺牲其他地区访客的解析速度。而像 .com、.net、.org 这些通用的域名后缀在全球绝大部分地区表现良好,而在中国大陆境内的解析速度则相对较慢,因为他们没有在大陆境内部署 Anycast 节点。在 DNS 冷启动的场景下(如果你的站点访客少,那几乎每次访问都是冷启动),首屏加载时间会因此增加 500ms 甚至更多。 经 v2ex 的网友 Showfom 提醒,GoDaddy 作为注册局掌握的部分 TLD 的 Nameserver 同样在中国大路境内拥有 Anycast 节点,比如 .one、.tv、.moe 等。另, Amazon Registry Services 旗下的 .you 域名经我测试也有境内的 Anycast 节点。其他域名后缀可自行测试。 你可以点击这里下载完整的测试结果 CSV 文件进行进一步的分析。

2025/11/25
articleCard.readMore

DNS 冷启动:小型站点的“西西弗斯之石”

当我们谈论网站性能时,我们通常关注前端渲染、资源懒加载、服务器响应时间(TTFB)等。然而,在用户浏览器真正开始请求内容之前,有一个至关重要却鲜少在性能优化方面被提及的部分—— DNS 解析。对于默默无闻的小型站点而言,“DNS Cache Miss”(缓存未命中)或我称之为“DNS 冷启动”,会成为绕不过去的性能瓶颈,也就是本文标题所提到的“西西弗斯之石”。 神话的隐喻:DNS 解析的漫长旅程 要理解这块“石头”的重量,我们必须重温 DNS 解析的完整路径。这并非一次简单的查找,而是一场跨越全球的接力赛: 起点:公共 DNS 服务器 — 用户发出请求,公共 DNS 服务器尝试在缓存中寻找答案。 首次“推石”:根服务器 — 缓存缺失(Cache Miss),公共 DNS 服务器被引向全球 13 组根服务器。 第二程:TLD 服务器 — 根服务器指向特定后缀(如 .com)的顶级域名服务器。 第三程:权威服务器 — TLD 服务器指向网站域名最终的“管家”——权威 DNS 服务器。 终点: 权威服务器返回最终的 IP 地址,再由公共 DNS 服务器返回给用户。 sequenceDiagram participant User as 用户/浏览器 participant Local as 本地DNS<br>递归解析器 participant Root as 根域名服务器 participant TLD as 顶级域服务器<br>(.com, .org等) participant Auth as 权威DNS服务器 Note over User,Auth: DNS递归查询完整流程 User->>Local: 1. 查询域名<br>www.example.com Note over Local: 检查缓存<br>未找到记录 Local->>Root: 2. 查询 .com 的TLD服务器 Root-->>Local: 3. 返回 .com TLD服务器地址 Local->>TLD: 4. 查询 example.com 的权威服务器 TLD-->>Local: 5. 返回 example.com 的权威服务器地址 Local->>Auth: 6. 查询 www.example.com 的A记录 Auth-->>Local: 7. 返回 IP地址 (e.g., 1.1.1.1) Note over Local: 缓存结果<br>(根据TTL设置) Local-->>User: 8. 返回最终IP地址 Note over User,Auth: 后续流程 User->>Auth: 9. 使用IP地址建立TCP连接<br>开始HTTP请求 对于首次或长时间未访问的请求,这个过程意味着至少 4 次网络往返(RTT),而在涉及到 CNAME 等情况时则会更多。对于那些拥有完美缓存的大型网站来说,这块石头可能已被别人推到了山顶;但对小型站点,它总是在山脚等待它的西西弗斯。 多重世界:Anycast 的镜像迷宫 “既然 DNS 冷启动的代价如此之高,那我能否使用脚本定时访问自己的网站,提前让公共 DNS 缓存预热起来呢?”——这是我曾经设想的解题思路。 然而,这一思路在现代互联网的 Anycast(泛播)架构下,往往徒劳无功。 Anycast 的核心理念是:同一个 IP 地址在全球多个节点同时存在,用户请求会被路由到“距离最近”或“网络路径最优”的节点。 这意味着,Google DNS (8.8.8.8) 、Cloudflare DNS (1.1.1.1)、阿里 DNS (223.5.5.5)、腾讯 DNS (119.29.29.29) 等公共 DNS 服务器背后并不是一台中心化的服务器,而是一组分布在世界各地、动态路由的节点集群。 于是问题出现了: 我在上海运行的预热脚本,也许命中了 223.5.5.5 的上海节点; 但来自北京的访问者,却会被路由到 223.5.5.5 的北京节点; 这两个节点的缓存,彼此独立、互不共享。 从站长的视角来看,DNS 缓存不再是一个可预测的实体,而是分裂成一片片地理隔离、随时可变的“镜像迷宫”。 每个访客都在不同的山脚下推着自己的那块石头,仿佛世界上有成千上万个西西弗斯,孤独地在各自的路径上前行。 不可控的缓存与「冷启动的常态化」 这也解释了为什么即便一个小型网站有规律地被脚本访问,仍可能在真实访客那里出现明显的 DNS 延迟。因为「预热」只是局部生效 —— 它温暖的是某一个任播节点的缓存,而不是整个网络的全貌。而当 TTL 到期或缓存被公共 DNS 服务器采用 LRU 等算法清理时,这份温度也会悄然散去。 从宏观上看,这让“小流量站点”陷入了某种宿命循环: 因访问量低,缓存不易命中; 因缓存不命中,解析耗时高; 因解析耗时高,首屏性能差,用户更少访问; 因用户更少访问,缓存更难命中。 冷启动不再是偶发的“意外”,而是一种被动的“常态”。 我们能否让石头变轻?—— 减缓冷启动影响的策略 西西弗斯的困境看似无解,但我们并非完全无能为力。虽然无法彻底消除 DNS 冷启动,但通过一系列策略,我们可以显著减轻这块石头的重量,缩短它每次滚落后被推上山顶的时间。 权衡的艺术:调整 DNS TTL (Time-To-Live) TTL(生存时间)是 DNS 记录中的一个关键值,它告知递归解析器(如公共 DNS、本地缓存)可以将一条解析记录缓存多久,尽管他们可能会被 LRU 算法淘汰。 拉长 TTL 可以有效提高缓存的命中率,减少 DNS 冷启动的情况,尽可能让西西弗斯之石保留在山顶上。 但拉长 TTL 是以牺牲灵活性作为代价的:如果你因为某些原因需要更换域名做对应的 IP 地址,过长的 TTL 可能会导致访客在很长一段时间内取得的都是已经失效的 IP 地址。 选择更快的“信使”:使用合适的权威 DNS 服务器 DNS 解析的最后一公里——从公共 DNS 服务器到你的权威 DNS 服务器——的耗时同样至关重要。如果你的域名所采用的 Nameserver 服务响应缓慢、全球节点稀少、又或者距离访客所请求的公共 DNS 服务器距离太远,那么即使用户的公共 DNS 节点就在身边,整个解析链条依然会被这最后一环拖慢。 如果我正在写的是一篇英文博客,那么我只需要说把 Nameserver 换成 Cloudflare、Google 等一线大厂就完事了。这些大厂提供免费的权威 DNS 托管业务,且在全球各地拥有大量节点,在这方面是非常专业且值得信赖的。 但我现在正在使用简体中文,根据我的博客统计数据,我的读者大多来自中国大陆,他们的站点访客大多也来自中国大陆,他们请求的公共 DNS 服务器大概率也都部署在中国大陆,而 Cloudflare/Google Cloud DNS 完全没有权威 DNS 服务器的中国大陆节点,这会拖慢速度。所以如果你的访客主要来自中国大陆境内,或许可以试试阿里云或者 Dnspod,他们主要的权威 DNS 服务器节点都在中国大陆境内,这在理论上可以减少公共 DNS 服务器与 权威 DNS 服务器之间的通信时长。 结语:推石头的人 DNS 冷启动的问题,从未有完美的解决方案。它像是互联网架构中注定存在的一段“延迟的诗意”——每个访问者都从自己的网络拓扑出发,沿着看不见的路径,一步步推着那块属于自己的石头,直到抵达你的服务器山顶,换得屏幕上第一个像素的亮起。 对小型站点而言,这或许是命运的重量;但理解它、优化它、监测它,便是我们在这条漫长上坡路上,为石头磨出更光滑的棱角。 参见 Performance Benefits | Public DNS | Google for Developers How do DNS queries affect website latency? - falconcloud.ae

2025/11/11
articleCard.readMore

HTTP/2 Server Push 已事实性“死亡”,我很怀念它

我最近一阵子在重构我的博客,恰巧之前一阵子准备秋招的时候背八股时看到了 HTTP/2 的服务端推送,于是便尝试在部署阶段为我的博客配置好 HTTP/2 的服务端推送,试图以此来进一步优化首屏加载速度。 HTTP/2 服务端推送为什么能提升首屏加载速度 如下图,在传统的 HTTP/1.1 中,浏览器会先下载 index.html 并完成第一轮解析,然后再从解析出的数据中拿到 css/js 资源的 url,再进行第二轮请求,在 tcp/tls 连接建立后最小需要两个 RTT 才能取回完整渲染页面所需的资源。 sequenceDiagram participant Browser participant Server Browser->>Server: GET /index.html Server-->>Browser: 200 OK + HTML Browser->>Server: GET /style.css Browser->>Server: GET /app.js Server-->>Browser: 200 OK + CSS Server-->>Browser: 200 OK + JS Note over Browser: 浏览器必须等 HTML 下载并解析后<br/>才能发起后续请求,增加往返延迟 (RTT) 而在 HTTP/2 的设想中,流程则是像下面这张图一样。当浏览器请求 index.html 时,服务端可以顺带将 css/js 资源一起推送给客户端,这样在 tcp/tls 连接建立后最小只需要一个 RTT 就可以将页面渲染所需的资源取回。 sequenceDiagram participant Browser participant Server Browser->>Server: GET /index.html Server-->>Browser: 200 OK + HTML Server-->>Browser: PUSH_PROMISE /style.css Server-->>Browser: PUSH_PROMISE /app.js Server-->>Browser: (推送) style.css + app.js 内容 Note over Browser: 浏览器收到资源前置推送<br/>减少请求轮次与首屏延迟 为了在 HTTP/1.1 中尽可能减少后续的请求,前端开发者尝试了非常多的优化手段,正如 Sukka 在《静态资源递送优化:HTTP/2 和 Server Push》一文中所讲: 关键资源、关键渲染路径、关键请求链的概念诞生已久,异步加载资源的概念可谓是老生常谈:懒加载图片、视频、iframe,乃至懒加载 CSS、JS、DOM,懒执行函数。但是,关键资源递送的思路却依然没有多少改变。 HTTP/2 的 Server Push 创造了新的资源递送思路,CSS/JS 等资源不用随着 html 一起递送也能在一个 RTT 内被传送到客户端,而这一部分资源可以被浏览器缓存起来,不被 html 那较短的 TTL 所限制。 初步方案 既然理清了 HTTP/2 服务端推送的优势,于是准备着手优化。我的博客是纯静态的,通过 DNS 进行境内外分流:境内流量会访问到 DMIT 一台带有 cmin2/9929 网络优化的 vps 上,通过 caddy 提供服务;境外流量则是直接打到 vercel,借助 Amazon 的 CDN 为全球网络提供边缘加速。网络架构大概是下面这个样子: graph TD A[博客访客] --> B[发起DNS解析请求] B --> C[DNSPod 服务] C -->|境内访客:智能分流| D[网络优化 VPS] C -->|境外访客:智能分流| E[Vercel 平台] D --> F[Caddy] F --> G[返回博客内容给境内访客] E --> H[返回博客内容给境外访客] Caddy 可以通过 http.handlers.push 模块实现 HTTP/2 的服务端推送,在 Caddyfile 中我们可以编写简单的推送逻辑,这没问题;vercel 平台则没有给开发者提供 HTTP/2 服务端推送的配置项,但好在我是静态博客,对平台依赖性不强,考虑迁移到 Cloudflare Workers,五年前就有开发者实现了。 客户端的支持情况 历史上主流浏览器引擎(Chrome/Chromium、Firefox、Edge、Safari)曾普遍支持服务器推送技术。 2020 年 11 月,谷歌宣布计划在其 Chrome 浏览器的 HTTP/2 及 gQUIC(后发展为HTTP/3)实现中移除服务器推送功能。 2022 年 10 月,谷歌宣布计划从Chrome浏览器中移除服务器推送功能,指出该扩展在实际应用中性能不佳、使用率低且存在更优替代方案。Chrome 106 成为首个默认禁用服务器推送的版本。 2024 年 10 月 29 日,Mozilla 发布了 Firefox 132版本,因“与多个网站存在兼容性问题”移除了对HTTP/2服务器推送功能的支持。 至此,主流浏览器对 HTTP/2 服务端推送(Server Push)的支持已全部终结。从最初被视为“减少往返延迟、优化首屏加载”的创新特性,到最终被全面弃用,HTTP/2 推送的生命周期不过短短数年,成为 Web 性能优化历史上的一次重要实验。 替代方案 1. HTTP 103 Early Hints 103 Early Hints 是对服务端推送最直接的“继任者”。它是一个信息性的 HTTP 状态码 (Informational Response),允许服务器在生成完整的 HTML 响应(例如,状态码为 200 OK)之前,先发送一个带有 Link 头部的“早期提示”响应。 这个 Link 头部可以告诉浏览器:“嘿,我还在准备主菜(HTML),但你可以先去把配菜(CSS、JS)准备好”。这样,浏览器就能利用服务器的“思考时间”提前开始下载关键资源或预热到所需源的连接,从而显著缩短首屏渲染时间。 与服务端推送的对比: 决策权在客户端:Early Hints 只是“提示”,浏览器可以根据自身缓存情况、网络状况等因素决定是否采纳该提示。这就解决了服务端推送最大的痛点——服务器无法知晓客户端缓存而导致推送冗余资源。 兼容性更好:它是一种更轻量、更易于中间代理服务器理解和传递的机制。 103 Early Hints 对动态博客很有意义,在后端进行计算之前先把需要的资源通过 103 响应告知浏览器,让浏览器先取回其他资源,再等待后端返回最终的 html;而对于我这种产物都是预构建好的静态博客,完全没有任何意义,网关有那个发 103 响应的闲工夫完全可以把 html 直接发过去了。 2. 资源提示(Resource Hints): Preload & Prefetch 早在服务端推送被弃用前,通过 <link>标签实现的资源提示就已经是前端性能优化的常用手段。它们将资源加载的提示声明在 HTML 中,由浏览器主导整个过程。 <link rel="preload">: 用于告诉浏览器当前页面必定会用到的资源,请以高优先级立即开始加载,但加载后不执行。比如,隐藏在 CSS 深处的字体文件或由 JS 动态加载的首屏图片。通过 Preload,可以确保这些关键资源能尽早被发现和下载,避免渲染阻塞。 <link rel="prefetch">: 用于告诉浏览器用户在未来可能访问的页面或用到的资源,请在浏览器空闲时以低优先级在后台下载。例如,在文章列表页 prefetch 用户最可能点击的文章页面的资源,从而实现近乎“秒开”的跳转体验。 Preload 和 Prefetch 将资源加载的控制权完全交给了开发者和浏览器,通过声明式的方式精细化管理资源加载的优先级和时机,是目前最成熟、应用最广泛的资源预加载方案,但仍然逃不过 2 RTT 的魔咒。 尾声:写给一个理想主义者的挽歌 写到最后,我终究是没能为我的博客配上 HTTP/2 服务端推送。 HTTP/2 Server Push 已事实性“死亡”,我很怀念它。 在一个理想模型里,当浏览器请求 HTML 时,服务器顺手将渲染所需的 CSS 和 JS 一并推来,将原本至少两次的往返(RTT)干脆利落地压缩为一次。这是一个如此直接、如此漂亮的解决方案,几乎是前端工程师面对首屏渲染延迟问题时梦寐以求的“银弹”。它背后蕴含的是一种雄心勃勃的魄力:试图由服务端一次性地、彻底地解决“关键请求链”的延迟问题。 但 Web 的世界终究不是一个理想的实验室。它充满了缓存、重复访问的用户、以及形形色色的网络环境。 服务端推送最大的魅力,在于它的“主动”,而它最大的遗憾,也恰恰源于这份“主动”。它无法知晓浏览器缓存中是否早已静静躺着那个它正准备满腔热情推送的 style.css 文件。为了那一小部分首次访问用户的极致体验,却可能要以浪费更多再次访问用户的宝贵带宽为代价。 Web 的演进最终选择了一条更稳妥、更具协作精神的道路。它将决策权交还给了最了解情况的浏览器,整个交互从服务器的“我推送给你”,变成了服务器的“我建议你拿”,再由浏览器自己定夺。这或许不够浪漫,不够极致,但它更普适,也更健壮。 所以,我依然会怀念那个雄心勃勃的 Server Push。它代表了一种对极致性能的纯粹追求,一种美好的技术理想主义。尽管它已悄然淡出历史舞台,但它所指向的那个关于“速度”的梦想,早已被 103 Early Hints 和 preload 以一种更成熟、更懂得权衡的方式继承了下来。 参见 Remove HTTP/2 Server Push from Chrome | Blog | Chrome for Developers HTTP/2 Server Push - Wikipedia 静态资源递送优化:HTTP/2 和 Server Push | Sukka's Blog Module http.handlers.push - Caddy Documentation How to Configure HTTP/2 Server Push on Cloudflare Workers Sites Intent to Remove: HTTP/2 and gQUIC server push

2025/11/5
articleCard.readMore

Nuxt Content v3 中数组字段的筛选困境与性能优化

Nuxt Content 是 Nuxt 生态中用于处理 Markdown、YAML 等内容的强大模块。最近,我在使用 Nuxt v4 + Nuxt Content v3 重构博客(原为 Hexo)时,遇到了一个棘手的问题:v3 版本的默认查询 API 并未直接提供对数组字段进行“包含”($contains)操作的支持。 例如,这是我的正在写的这篇博客的 Front Matter: --- title: Nuxt Content v3 中数组字段的筛选困境 date: 2025-10-20 21:52:59 sticky: tags: - Nuxt - Nuxt Content - JavaScript --- 我的目标是创建一个 Tag 页面,列出所有包含特定 Tag(例如 'Nuxt')的文章。 v2 的便捷与 v3 的限制 在 Nuxt Content v2 中,数据基于文件系统存储,查询方式是对文件内容的抽象,模拟了类似 MongoDB 的 JSON 文档查询语法。我们可以轻松地使用 $contains 方法获取所有包含 “Nuxt” 标签的文章: const tag = decodeURIComponent(route.params.tag as string) const articles = await queryContent('posts') .where({ tags: { $contains: tag } }) // ✅ v2 中的 MongoDB Style 查询 .find() 但在使用 Nuxt Content v3 的 queryCollection API 时,我们很自然地会尝试使用 .where() 方法进行筛选: const tag = decodeURIComponent(route.params.tag as string) const { data } = await useAsyncData(`tag-${tag}`, () => queryCollection('posts') .where(tag, 'in', 'tags') // ❌ 这样会报错,因为第一次参数必须是字段名 .order('date', 'DESC') .select('title', 'date', 'path', 'tags') .all() ) 遗憾的是,这样是行不通的。.where() 的方法签名要求字段名必须作为首个参数传入:where(field: keyof Collection | string, operator: SqlOperator, value?: unknown)。 由于 Nuxt Content v3 底层采用 SQLite 作为本地数据库,所有查询都必须遵循类 SQL 语法。如果设计时未提供针对数组字段的内置操作符(例如 $contains 的 SQL 等价形式),最终的解决方案往往会显得比较“别扭”。 初版实现:牺牲性能的“全量拉取” 本着“尽快重构,后续优化”的思路,我写出了以下代码: // 初版实现:全量拉取后使用 JS 筛选 const allPosts = ( await useAsyncData(`tag-${route.params.tag}`, () => queryCollection('posts') .order('date', 'DESC') .select('title', 'date', 'path', 'tags') .all() ) ).data as Ref<Post[]> const Posts = computed(() => { return allPosts.value.filter(post => typeof post.tags?.map === 'function' ? post.tags?.includes(decodeURIComponent(route.params.tag as string)) : false ) }) 这种方法虽然满足了需求,但也带来了明显的性能代价:_payload.json 文件体积的膨胀。 在 Nuxt 项目中,_payload.json 用于存储 useAsyncData 的结果等动态数据。在全量拉取的方案下,每一个 Tag 页面 都会加载包含所有文章信息的 _payload.json,造成数据冗余。很多 Tag 页面仅需一两篇文章的数据,却被迫加载了全部文章信息,严重影响了性能。 讨巧方案:利用 SQLite 的存储特性进行优化 为了减少 useAsyncData 返回的查询结果,我查阅了 Nuxt Content 的 GitHub Discussions,发现在 v3.alpha.8 版本时就有人提出了一种“巧妙”的解决方案。 由于 Nuxt Content v3 使用 SQLite 数据库,原本在 Front Matter 中定义的 tags 数组(通过 z.array() 定义)最终会以 JSON 字符串的形式存储在数据库中(具体格式可在 .nuxt/content/sql_dump.txt 文件中查看)。 这意味着我们可以利用 SQLite 的字符串操作特性,通过 LIKE 动词配合通配符来完成数组包含的筛选,本质上是查询 JSON 字符串是否包含特定子串: const tag = decodeURIComponent(route.params.tag as string) const { data } = await useAsyncData(`tag-${route.params.tag}`, () => queryCollection('posts') .where('tags', 'LIKE', `%"${tag}"%`) .order('date', 'DESC') .select('title', 'date', 'path', 'tags') .all() ) 下面是优化后重新生成的文件占用,体积减小还是非常显著的 tags 目录体积: 2.9MiB -> 1.4MiB 单个 _payload.json 的体积: 23.1KiB -> 1.01 KiB 通过这种方法,我们成功将查询逻辑下推到了数据库层,避免了不必要的全量数据传输,显著降低了单个目录中 _payload.json 的体积,实现了性能优化。 参见 queryCollection - Nuxt Content How do you query z.array() fields (e.g. tags) in the latest nuxt-content module (v3.alpha.8) · nuxt/content · Discussion #2955

2025/10/20
articleCard.readMore

后 OCSP 时代,浏览器如何应对证书吊销新挑战

2023 年 8 月,CA/Browser Forum 通过了一项投票——不再强制要求 Let’s Encrypt 等公开信任的 CA 设立 OCSP Server 2024 年 7 月,Let's Encrypt 发布博客,披露其计划关闭 OCSP Server 同年 12 月,Let's Encrypt 发布其关闭 OCSP Server 的时间计划表,大致情况如下: 2025 年 1 月 30 日 - Let’s Encrypt 不再接受新的包含 OCSP Must-Staple 扩展的证书签发请求,除非你的账号先前申请过此类证书 2025 年 5 月 7 日 - Let's Encrypt 新签发的证书将加入 CRL URLs,不再包含 OCSP URLs,并且所有新的包含 OCSP Must-Staple 扩展的证书签发请求都将被拒绝 2025 年 8 月 6 日 - Let's Encrypt 关闭 OCSP 服务器 Let's Encrypt 是全世界最大的免费 SSL 证书颁发机构,而这一举动标志着我们已逐渐步入后 OCSP 时代。 OCSP 的困境:性能与隐私的权衡 Let's Encrypt 这一举动的背后,是人们对 OCSP(在线证书状态协议)长久以来累积的不满。OCSP 作为一种实时查询证书有效性的方式,最初的设想很美好:当浏览器访问一个网站时,它可以向 CA(证书颁发机构) 的 OCSP 服务器发送一个简短的请求,询问该证书是否仍然有效。这似乎比下载一个巨大的 CRL(证书吊销列表) 要高效得多。 然而,OCSP 在实际应用中暴露出众多缺陷: 首先是性能问题。尽管单个请求很小,但当数百万用户同时访问网站时,OCSP 服务器需要处理海量的实时查询。这不仅给 CA 带来了巨大的服务器压力,也增加了用户访问网站的延迟。如果 OCSP 服务器响应缓慢甚至宕机,浏览器可能会因为无法确认证书状态而中断连接,或者为了用户体验而不得不“睁一只眼闭一只眼”,这都削弱了 OCSP 的安全性。 更严重的是隐私问题。每一次 OCSP 查询,都相当于向 CA 报告了用户的访问行为。这意味着 CA 能够知道某个用户在何时访问了哪个网站。虽然 OCSP 查询本身不包含个人身份信息,但将这些信息与 IP 地址等数据结合起来,CA 完全可以建立起用户的浏览习惯画像。对于重视隐私的用户和开发者来说,这种“无声的监视”是不可接受的。即使 CA 故意不保留这些信息,地区法律也可能强制 CA 收集这些信息。 再者,OCSP 还存在设计上的安全缺陷。由于担心连接超时影响用户体验,浏览器通常默认采用 soft-fail 机制:一旦无法连接 OCSP 服务器,便会选择放行而非阻断连接。攻击者恰恰可以利用这一点,通过阻断客户端与 OCSP 服务器之间的通信,使查询始终超时,从而轻松绕过证书状态验证。 OCSP 装订 (OCSP stapling) 基于上面这些缺陷,我们有了 OCSP 装订 (OCSP stapling) 方案,这在我去年的博客里讲过,欢迎回顾。 强制 OCSP 装订 (OCSP Must-Staple) OCSP Must-Staple 是一个在 ssl 证书申请时的拓展项,该扩展会告知浏览器:若在证书中识别到此扩展,则不得向证书颁发机构发送查询请求,而应在握手阶段获取装订式副本。若未能获得有效副本,浏览器应拒绝连接。 这项功能赋予了浏览器开发者 hard-fail 的勇气,但在 OCSP 淡出历史之前,Let's Encrypt 似乎是唯一支持这一拓展的主流 CA,并且这项功能并没有得到广泛使用。 ~~本来不想介绍这项功能的(因为根本没人用),但考虑到这东西快入土了,还是给它在中文互联网中立个碑,~~更多信息参考 Let's Encrypt 的博客。 Chromium 的方案:弱水三千只取一瓢 OCSP 的隐私和性能问题并非秘密,浏览器厂商们早就开始了各自的探索。2012 年,Chrome 默认禁用了 CRLs、OCSP 检查,转向自行设计的证书校验机制。 众所周知,吊销列表可以非常庞大。如果浏览器需要下载和解析一个完整的全球吊销列表,那将是一场性能灾难(Mozilla 团队在今年的博客中提到,从 3000 个活跃的 CRL 下载的文件大小将达到 300MB)。Chromium 团队通过分析历史数据发现,大多数被吊销的证书属于少数高风险类别,例如证书颁发机构(CA)本身被攻破、或者某些大型网站的证书被吊销。基于此洞察,CRLSets 采取了以下策略: 分层吊销:Chromium 不会下载所有被吊销的证书信息,而是由 Google 团队维护一个精简的、包含“最重要”吊销信息的列表。这个列表会定期更新并通过 Chrome 浏览器更新推送给用户。 精简高效:这个列表体积非常小,目前大概只有 600KB。它包含了那些一旦被滥用就会造成大规模安全事故的证书,例如 CA 的中间证书、或者一些知名网站(如 Google、Facebook)的证书。 牺牲部分安全性:这种方案的缺点也很明显——它无法覆盖所有的证书吊销情况。对于一个普通网站的证书被吊销,CRLSets 大概率无法检测到。根据 Mozilla 今年的博客所说,CRLSets 只包含了 1%~2% 的未过期的被吊销证书信息。 虽然 CRLSets 是一种“不完美”的解决方案,但它在性能和可用性之间找到了一个平衡点。它确保了用户在访问主流网站时的基础安全,同时避免了 OCSP 带来的性能和隐私开销。对于 Chromium 而言,与其追求一个在现实中难以完美实现的 OCSP 方案,不如集中精力解决最紧迫的安全威胁。 Firefox 的方案:从 CRLs 到 CRLite 与 Chromium 的“只取一瓢”策略不同,Firefox 的开发者们一直在寻找一种既能保证全面性,又能解决性能问题的方案。 为了解决这个问题,Mozilla 提出了一个创新的方案:CRLite。CRLite 的设计理念是通过哈希函数和布隆过滤器等数据结构,将庞大的证书吊销列表压缩成一个小巧、可下载且易于本地验证的格式。 CRLite 的工作原理可以简单概括为: 数据压缩:CA 定期生成其全部吊销证书的列表。 服务器处理:Mozilla 的服务器会收集这些列表,并使用加密哈希函数和布隆过滤器等技术,将所有吊销证书的信息编码成一个非常紧凑的数据结构。 客户端验证:浏览器下载这个压缩文件,当访问网站时,只需本地对证书进行哈希计算,然后查询这个本地文件,就能快速判断该证书是否已被吊销。 与 CRLSets 相比,CRLite 的优势在于它能够实现对所有吊销证书的全面覆盖,同时保持极小的体积。更重要的是,它完全在本地完成验证,这意味着浏览器无需向任何第三方服务器发送请求,从而彻底解决了 OCSP 的隐私问题。 Firefox 当前的策略为每 12 小时对 CRLite 数据进行一次增量更新,每日的下载数据大约为 300KB;每 45 天进行一次全量的快照同步,下载数据约为 4MB。 Mozilla 开放了他们的数据看板,你可以在这里找到近期的 CRLite 数据大小:https://yardstick.mozilla.org/dashboard/snapshot/c1WZrxGkNxdm9oZp7xVvGUEFJCELfApN 自 2025 年 4 月 1 日发布的 Firefox Desktop 137 版本起,Firefox 开始逐步以 CRLite 替换 OCSP 校验;同年 8 月 19 日,Firefox Desktop 142 针对 DV 证书正式弃用 OCSP 检验。 CRLite 已经成为 Firefox 未来证书吊销验证的核心方案,它代表了对性能、隐私和安全性的全面追求。 后 OCSP 时代的展望 随着 Let's Encrypt 等主要 CA 关闭 OCSP 服务,OCSP 的时代正在加速落幕。我们可以看到,浏览器厂商们已经开始各自探索更高效、更安全的替代方案。 Chromium 凭借其 CRLSets 方案,在性能和关键安全保障之间取得了务实的平衡。 Firefox 则通过 CRLite 这一技术创新,试图在全面性、隐私和性能三者之间找到最佳的解决方案。 这些方案的共同点是:将证书吊销验证从实时在线查询(OCSP)转变为本地化验证,从而规避了 OCSP 固有的性能瓶颈和隐私风险。 未来,证书吊销的生态系统将不再依赖单一的、中心化的 OCSP 服务器。取而代之的是,一个更加多元、分布式和智能化的新时代正在到来。OCSP 这一技术可能逐渐被淘汰,但它所试图解决的“证书吊销”这一核心安全问题,将永远是浏览器和网络安全社区关注的重点。 参见 CRLite: Fast, private, and comprehensive certificate revocation checking in Firefox - Mozilla Hacks - the Web developer blog The Slow Death of OCSP | Feisty Duck mozilla/crlite: Compact certificate revocation lists for the WebPKI OCSP Service Has Reached End of Life - Let's Encrypt Ending OCSP Support in 2025 - Let's Encrypt Intent to End OCSP Service - Let's Encrypt CRLSets - The Chromium Projects Google Chrome Will No Longer Check for Revoked SSL Certificates Online | PCWorld Chrome does certificate revocation better | ZDNET 主流客户端/浏览器证书吊销验证机制技术对与分析 | 帽之岛, Hat's Land OCSP 的淡出… – Gea-Suan Lin's BLOG

2025/10/16
articleCard.readMore

初试 Github Action Self-hosted Runner,想说爱你不容易

在今年八月的时候,我这边所在的一个 Github Organization 在私有项目开发阶段频繁触发 CI,耗尽了 Github 为免费计划 (Free Plan) 提供的每月 2000 分钟 Action 额度(所有私有仓库共享,公有仓库不计)。大致看了下,CI 流设置得是合理的,那么就要另寻他法看看有没有办法去提供更宽裕的资源,因此也就盯上了文章标题中所提到的 Github Action Self-hosted Runner。 对于这个 Self-hosted Runner,与 Github 官方提供的 runner 相比,主要有以下几个优势 针对私有仓库,拥有无限制的 Action 运行时长 可以自行搭配更强大的硬件计算能力和内存 可以接入内网环境,方便与内网/局域网设备通信 配置安装 由于不清楚需要的网络环境,我这次测试直接选用了一台闲置的香港 vps,4核4G + 80G 硬盘 + 1Gbps 大口子的配置,除了硬盘读写稍微拉胯一些,别的地方可以说是拉满了。 Self-hosted Runner 的配置本身是相当直接和清晰的,照着官方提供的方案基本没什么问题。 三个主流平台都有,如果好好加以利用,应该可以涵盖包括 iPhone 应用打包等一系列的需求。 在观察一下我这边拿到手的 2.328.0 版本的 runner 安装文件压缩包的体积在 220MB 左右,内置了 node20 和 node24 各两个版本的运行环境。 在执行完 config.sh 后,当前目录下就会多出一个 svc.sh,可以帮助利用这东西来调用 systemd 实现进程守护之类的需求。 再次刷新网页,就可以看到 Self-hosted Runner 处于已经上线的状态了 指定 Action 采用自己的 Runner 这一步很简单,只需在原 Action 的 yml 文件中改变 runs-on 字段即可 jobs: run: + runs-on: self-hosted - runs-on: ubuntu-latest 实测 当我满心欢喜地将 CI 流程从 Github 官方的 runner 切换到自托管的 runner 后,问题很快就浮现了,而这也正是我“爱不起来”的主要原因。问题集中体现在我习以为常的 setup-python 这一由 Github 官方维护的 Github Action Flow 中,提示 3.12 版本没找到。 在 Github 官方提供的虚拟环境中,这些 Action 会为我们准备好指定版本的开发环境。例如,uses: actions/setup-python 加上 with: python-version: '3.12' 就会自动在环境中安装并配置好 Python 3.12.x。我对此已经习以为常,认为这是一个“开箱即用”的功能。但在 Self-hosted Runner 上,情况略有些不同。setup-python 在文档中指出 Python distributions are only available for the same environments that GitHub Actions hosted environments are available for. If you are using an unsupported version of Ubuntu such as 19.04 or another Linux distribution such as Fedora, setup-python may not work. setup-python 这个 Action 只支持 Github Action 所采用的同款操作系统,而我 VPS 的 Debian 不受支持,因此有这个误报,同时也给我的 Debian 判了死刑。 症结所在:对 Self-hosted Runner 的误解 我潜意识里认为,Self-hosted Runner 仅仅是将计算成本从 Github 服务器转移到了本地,而 actions/setup-python 这种官方标准动作,理应会像 Github-hosted Runner 中那样,优雅地为我下载、安装、并配置好我需要的一切。然而,Self-hosted Runner 的本质只是从 Github 接收任务,并在当前的操作系统环境中执行指令,并不保证和 Github 官方提供的 Runner 的运行环境一致。 Self-hosted Runner 不是一个开箱即用的“服务”,而是一个需要你亲自管理的“基础设施”。你需要负责服务器的安装、配置、安全更新、依赖管理、磁盘清理等一系列运维工作。它更适合那些对 CI/CD 有更高阶需求的团队或个人:比如 CI/CD 消费大户、需要特定硬件(如 ARM、GPU)进行构建的团队、或者 CI 流程深度依赖内部网络资源的企业。对于像我这样只是愿意拿出更多的本地计算资源来获取更多 Action 运行时长的普通开发者而言,它带来的运维心智负担,似乎是有一点重了。

2025/9/5
articleCard.readMore

DNS 解析延迟毁了我的图床优化

去年夏天,我花了不少时间搭建博客图床,核心目标是分地区解析 DNS,让国内外访客都能快速加载图片。技术方案看起来完美无缺,直到最近群友反馈首次访问时图片加载很慢,我才发现问题所在。 955 毫秒的 DNS 解析时长! 这个数字让我大吃一惊。访客点开博客后,光是确定图片服务器位置就要等将近一秒,这完全抵消了 CDN 优化的效果。 为什么之前没发现? 主要是 DNS 缓存的"功劳"。它会为后续访问记住解析结果,让我的本地测试和复访测试看起来都很正常。直到用户反馈,结合最近准备秋招复习的 DNS 解析流程(递归查询、权威查询、根域名、顶级域名等),我才定位到问题:首次访问时的 DNS 解析延迟。 DNS 解析流程分析 让我们看看访客访问 static.031130.xyz 时,DNS 是如何工作的: sequenceDiagram participant User as 访客浏览器 participant Local as 本地 DNS participant CF as Cloudflare<br/>(国外权威) participant DP as DNSPod<br/>(国内权威) participant CDN as CDN 节点 User->>Local: 请求 static.031130.xyz Local->>CF: 查询 031130.xyz 权威 Note over Local,CF: 跨国查询,延迟高 CF->>Local: CNAME: cdn-cname.zhul.in Local->>DP: 查询 zhul.in 权威 DP->>Local: CNAME: small-storage-cdn.b0.aicdn.com Local->>DP: 查询 aicdn.com DP->>Local: CNAME: nm.aicdn.com Local->>DP: 查询最终 IP DP->>Local: 返回 CDN IP Local->>User: 返回解析结果 User->>CDN: 连接并下载图片 问题就在这里:前两步查询指向了国外的 Cloudflare 权威服务器。对于国内用户,虽然最终解析到的 CDN 节点是国内的,但跨国 DNS 查询就足以拖垮首次访问体验。那 955ms 的延迟,基本都耗在与国外 DNS 服务器的通信上了。 优化方案 针对这个问题,我采取了三个措施: 1. DNS 预取 在博客 HTML 的 <head> 中添加: <link rel="dns-prefetch" href="//static.031130.xyz"> 这样浏览器在渲染页面时就会提前解析图床域名,等真正需要加载图片时,DNS 结果可能已经准备好了。 2. 延长 TTL 将 static.031130.xyz 的 CNAME 记录 TTL 值调大(从几分钟延长到几小时甚至一天)。这样本地 DNS 服务器会缓存更久,后续用户可以直接使用缓存结果,省掉权威查询。 3. 迁移权威 DNS(核心) 将 031130.xyz 域名的权威 DNS 服务器从 Cloudflare 迁移到国内的 DNSPod: graph TB subgraph "优化前" A1[访客] --> B1[本地 DNS] B1 --> C1[Cloudflare 权威<br/>国外] C1 --> D1[DNSPod 权威<br/>国内] D1 --> E1[CDN 节点] style C1 fill:#ffcccc end graph TB subgraph "优化后" A2[访客] --> B2[本地 DNS] B2 --> D2[DNSPod 权威<br/>国内] D2 --> E2[CDN 节点] style D2 fill:#ccffcc end 迁移后的好处: 递归 DNS 查询 031130.xyz 时,直接找到国内的 DNSPod,响应快 DNSPod 直接返回 static.031130.xyz -> small-storage-cdn.b0.aicdn.com,无需中间跳转 整个 DNS 解析链路在国内完成,首次访问延迟大幅降低 优化效果 虽然 DNS 缓存给测试带来了困难,但迁移权威 DNS + 调整 TTL + 添加预取后,首次访问的 DNS 解析时间降到了可接受的范围。 经验教训 DNS 位置很重要:涉及多地优化时,权威 DNS 的地理位置对首次访问延迟影响很大。优先使用国内权威服务器。 首次访问是关键:虽然缓存能帮助后续访问,但首次访问体验直接影响用户印象。善用 dns-prefetch 和合理的 TTL 设置。 监控和反馈重要:本地测试环境往往有缓存加持,真实的首次访问体验需要通过监控和用户反馈来发现。 重要提醒:警惕 CNAME 拉平 如果你需要分地区解析来让访客连接到最近的 CDN 节点,务必避开 CNAME Flattening(CNAME 拉平)。 什么是 CNAME 拉平? 权威 DNS 服务器(如 Cloudflare)看到 CNAME 记录后,会主动查询目标域名的最终 IP 地址,然后直接返回 IP 而不是 CNAME。 为什么会出问题? 分地区解析(GeoDNS)在权威 DNS 服务器层面实现。当权威服务器执行 CNAME 拉平时,它会在自己的位置查询目标域名的 IP。如果权威 DNS 在美国,它获取的 IP 就是美国最优节点,然后把这个 IP 返回给所有地区的查询者,包括中国用户。这样,你为中国用户配置的国内 CDN IP 策略就完全失效了。 graph LR subgraph "启用 CNAME 拉平的问题" A[中国用户] --> B[Cloudflare 权威<br/>美国节点] B --> C[查询目标 CNAME] C --> D[返回美国 CDN IP] D --> A style D fill:#ffcccc end 正确做法 老实使用 CNAME 指向另一个支持 GeoDNS 的域名(如 static.031130.xyz -> cdn-cname.zhul.in,后者在 DNSPod 上做分地区解析),才能保证分流策略正确执行。 如果需要分地区解析功能,不要在相关域名上启用 CNAME Flattening(或 ALIAS、ANAME 等类似功能)。

2025/8/11
articleCard.readMore

Vue Markdown 渲染优化实战(下):告别 DOM 操作,拥抱 AST 与函数式渲染

上回回顾:当 morphdom 遇上 Vue 在上一篇文章中,我们经历了一场 Markdown 渲染的性能优化之旅。从最原始的 v-html 全量刷新,到按块更新,最终我们请出了 morphdom 这个“终极武器”。它通过直接比对和操作真实 DOM,以最小的代价更新视图,完美解决了实时渲染中的性能瓶颈和交互状态丢失问题。 然而,一个根本性问题始终存在:在 Vue 的地盘里,绕过 Vue 的虚拟 DOM (Virtual DOM) 和 Diff 算法,直接用一个第三方库去“动刀”真实 DOM,总感觉有些“旁门左道”。这就像在一个精密的自动化工厂里,引入了一个老师傅拿着锤子和扳手进行手动修补。虽然活干得漂亮,但总觉得破坏了原有的工作流,不够“Vue”。 那么,有没有一种更优雅、更“原生”的方式,让我们既能享受精准更新的快感,又能完全融入 Vue 的生态体系呢? 带着这个问题,我询问了前端群里的伙伴们。 如果就要做一个渲染器,你这个思路不是最佳实践。每次更新时,你都生成全量的虚拟 HTML,然后再对 HTML 做减法来优化性能。然而,每次更新的增量部分是明确的,为什么不直接用这部分增量去做加法?增量部分通过 markdown-it 的库无法直接获取,但更好的做法是在这一步进行改造:先解析 Markdown 的结构,再利用 Vue 的动态渲染能力生成 DOM。这样,DOM 的复用就可以借助 Vue 自身的能力来实现。—— j10c 可以用 unified 结合 remark-parse 插件,将 markdown 字符串解析为 ast,然后根据 ast 使用 render func 进行渲染即可。—— bii & nekomeowww 新思路:从“字符串转换”到“结构化渲染” 我们之前的方案,无论是 v-html 还是 morphdom,其核心思路都是: Markdown 字符串 -> markdown-it -> HTML 字符串 -> 浏览器/morphdom -> DOM 这条链路的问题在于,从 HTML 字符串 这一步开始,我们就丢失了 Markdown 的原始结构信息。我们得到的是一堆非结构化的文本,Vue 无法理解其内在逻辑,只能将其囫囵吞下。 而新的思路则是将流程改造为: Markdown 字符串 -> AST (抽象语法树) -> Vue VNodes (虚拟节点) -> Vue -> DOM 什么是 AST? AST (Abstract Syntax Tree) ,即抽象语法树,是源代码或标记语言的结构化表示。它将一长串的文本,解析成一个层级分明的树状对象。对于 Markdown 来说,一个一级标题会变成一个 type: 'heading', depth: 1 的节点,一个段落会变成一个 type: 'paragraph' 的节点,而段落里的文字,则是 paragraph 节点的 children。 一旦我们将 Markdown 转换成 AST,就相当于拥有了整个文档的“结构图纸”。我们不再是面对一堆模糊的 HTML 字符串,而是面对一个清晰、可编程的 JavaScript 对象。 我们的新工具:unified 与 remark 为了实现 Markdown -> AST 的转换,我们引入 unified 生态。 unified: 一个强大的内容处理引擎。你可以把它想象成一条流水线,原始文本是原料,通过添加不同的“插件”来对它进行解析、转换和序列化。 remark-parse: 一个 unified 插件,专门负责将 Markdown 文本解析成 AST(具体来说是 mdast 格式)。 第一步:将 Markdown 解析为 AST 首先,我们需要安装相关依赖: npm install unified remark-parse 然后,我们可以轻松地将 Markdown 字符串转换为 AST: import { unified } from 'unified' import remarkParse from 'remark-parse' const markdownContent = '# Hello, AST!\n\nThis is a paragraph.' // 创建一个处理器实例 const processor = unified().use(remarkParse) // 解析 Markdown 内容 const ast = processor.parse(markdownContent) console.log(JSON.stringify(ast, null, 2)) 运行以上代码,我们将得到一个如下所示的 JSON 对象,这就是我们梦寐以求的 AST: { "type": "root", "children": [ { "type": "heading", "depth": 1, "children": [ { "type": "text", "value": "Hello, AST!", "position": { ... } } ], "position": { ... } }, { "type": "paragraph", "children": [ { "type": "text", "value": "This is a paragraph.", "position": { ... } } ], "position": { ... } } ], "position": { ... } } 第二步:从 AST 到 Vue VNodes 拿到了 AST,下一步就是将这个“结构图纸”真正地“施工”成用户可见的界面。在 Vue 的世界里,描述 UI 的蓝图就是虚拟节点 (VNode),而 h() 函数(即 hyperscript)就是创建 VNode 的画笔。 我们的任务是编写一个渲染函数,它能够递归地遍历 AST,并为每一种节点类型(heading, paragraph, text 等)生成对应的 VNode。 下面是一个简单的渲染函数实现: function renderAst(node) { if (!node) return null switch (node.type) { case 'root': return h('div', {}, node.children.map(renderAst)) case 'paragraph': return h('p', {}, node.children.map(renderAst)) case 'text': return node.value case 'emphasis': return h('em', {}, node.children.map(renderAst)) case 'strong': return h('strong', {}, node.children.map(renderAst)) case 'inlineCode': return h('code', {}, node.value) case 'heading': return h('h' + node.depth, {}, node.children.map(renderAst)) case 'code': return h('pre', {}, [h('code', {}, node.value)]) case 'list': return h(node.ordered ? 'ol' : 'ul', {}, node.children.map(renderAst)) case 'listItem': return h('li', {}, node.children.map(renderAst)) case 'thematicBreak': return h('hr') case 'blockquote': return h('blockquote', {}, node.children.map(renderAst)) case 'link': return h('a', { href: node.url, target: '_blank' }, node.children.map(renderAst)) default: // 其它未实现类型 return h('span', { }, `[${node.type}]`) } } 第三步:封装 Vue 组件 整合上述逻辑,我们可以构建一个 Vue 组件。鉴于直接生成 VNode 的特性,采用函数式组件或显式 render 函数最为适宜。 <template> <component :is="VNodeTree" /> </template> <script setup> import { computed, h, shallowRef, watchEffect } from 'vue' import { unified } from 'unified' import remarkParse from 'remark-parse' const props = defineProps({ mdText: { type: String, default: '' } }) const ast = shallowRef(null) const parser = unified().use(remarkParse) watchEffect(() => { ast.value = parser.parse(props.mdText) }) // AST 渲染函数 (同上文 renderAst 函数) function renderAst(node) { ... } const VNodeTree = computed(() => renderAst(ast.value)) </script> 现在就可以像使用普通组件一样使用它了: <template> <MarkdownRenderer :mdText="markdownContent" /> </template> <script setup> import { ref } from 'vue' import MarkdownRenderer from './MarkdownRenderer.vue' const markdownContent = ref('# Hello Vue\n\nThis is rendered via AST!') </script> AST 方案的巨大优势 切换到 AST 赛道后,我们获得了前所未有的超能力: 原生集成,性能卓越:我们不再需要 v-html 的暴力刷新,也不再需要 morphdom 这样的“外援”。所有更新都交由 Vue 自己的 Diff 算法处理,这不仅性能极高,而且完全符合 Vue 的设计哲学,是真正的“自己人”。 高度灵活性与可扩展性:AST 作为可编程的 JavaScript 对象,为定制化处理提供了坚实基础: 元素替换:可将原生元素(如 <h2>)无缝替换为自定义 Vue 组件(如 <FancyHeading>),仅在 renderAst 函数中调整对应 case 逻辑即可。 逻辑注入:可便捷地为外部链接 <a> 添加 target="_blank" 与 rel="noopener noreferrer" 属性,或为图片 <img> 包裹懒加载组件,此类操作在 AST 层面易于实现。 生态集成:充分利用 unified 丰富的插件生态(如 remark-gfm 支持 GFM 语法,remark-prism 实现代码高亮),仅需在处理器链中引入相应插件(.use(pluginName))。 关注点分离:解析逻辑(remark)、渲染逻辑(renderAst)和业务逻辑(Vue 组件)被清晰地分离开来,代码结构更清晰,维护性更强。 类型安全与可预测性:相较于操作字符串或原始 HTML,基于结构化 AST 的渲染逻辑更易于进行类型校验与逻辑推理。 结论:从功能实现到架构优化的演进 回顾优化历程: v-html:实现简单,但存在性能与安全性隐患。 分块更新:缓解了部分性能问题,但方案存在局限性。 morphdom:有效提升了性能与用户体验,但与 Vue 核心机制存在隔阂。 AST + 函数式渲染:回归 Vue 原生范式,提供了性能、灵活性、可维护性俱佳的终极解决方案。 通过采用 AST,我们不仅解决了具体的技术挑战,更重要的是实现了思维范式的转变——从面向结果(HTML 字符串)的编程,转向面向过程与结构(AST)的编程。这使我们能够深入内容本质,从而实现对渲染流程的精确控制。 本次从“全量刷新”到“结构化渲染”的优化实践,不仅是一次性能提升的技术过程,更是一次深入理解现代前端工程化思想的系统性探索。最终实现的 Markdown 渲染方案,在性能、功能性与架构优雅性上均达到了较高水准。

2025/7/13
articleCard.readMore

Vue Markdown 渲染优化实战(上):从暴力刷新、分块更新到 Morphdom 的华丽变身

需求背景 在最近接手的 AI 需求中,需要实现一个类似 ChatGPT 的对话交互界面。其核心流程是:后端通过 SSE(Server-Sent Events)协议,持续地将 AI 生成的 Markdown 格式文本片段推送到前端。前端负责动态接收并拼接这些 Markdown 片段,最终将拼接完成的 Markdown 文本实时渲染并显示在用户界面上。 Markdown 渲染并不是什么罕见的需求,尤其是在 LLM 相关落地产品满天飞的当下。不同于 React 生态拥有一个 14k+ star 的著名第三方库——react-markdown,Vue 这边似乎暂时还没有一个仍在活跃维护的、star 数量不低(起码得 2k+ 吧?)的 markdown 渲染库。cloudacy/vue-markdown-render 最后一次发版在一年前,但截止本文写作时间只有 103 个 star;miaolz123/vue-markdown 有 2k star,但最后一次 commit 已经是 7 年前了;zhaoxuhui1122/vue-markdown 更是 archived 状态。 第一版方案:简单粗暴的 v-html 简单调研了一圈,发现 Vue 生态里确实缺少一个能打的 Markdown 渲染库。既然没有现成的轮子,那咱就自己造一个! 根据大部分文章以及 LLM 的推荐,我们首先采用 markdown-it 这个第三方库将 markdown 转换为 html 字符串,再通过 v-html 传入。 PS: 我们这里假设 Markdown 内容是可信的(比如由我们自己的 AI 生成)。如果内容来自用户输入,一定要使用 DOMPurify 这类库来防止 XSS 攻击,避免给网站“开天窗”哦! 示例代码如下: <template> <div v-html="renderedHtml"></div> </template> <script setup> import { computed, onMounted, ref } from 'vue'; import MarkdownIt from 'markdown-it'; const markdownContent = ref(''); const md = new MarkdownIt(); const renderedHtml = computed(() => md.render(markdownContent.value)) onMounted(() => { // markdownContent.value = await fetch() ... }) </script> 进化版:给 Markdown 分块更新 上述方案虽然能实现基础渲染,但在实时更新场景下存在明显缺陷:每次接收到新的 Markdown 片段,整个文档都会触发全量重渲染。即使只有最后一行是新增内容,整个文档的 DOM 也会被完全替换。这导致两个核心问题: **性能顶不住:**Markdown 内容增长时,markdown-it 解析和 DOM 重建的开销呈线性上升。 **交互状态丢失:**全量刷新会把用户当前的操作状态冲掉。最明显的就是,如果你选中了某段文字,一刷新,选中状态就没了! 为了解决这两个问题,我们在网上找到了分块渲染的方案 —— 把 Markdown 按两个连续的换行符 (\n\n) 切成一块一块的。这样每次更新,只重新渲染最后一块新的,前面的老块直接复用缓存。好处很明显: 用户如果选中了前面块里的文字,下次更新时选中状态不会丢(因为前面的块没动)。 需要重新渲染的 DOM 变少了,性能自然就上来了。 代码调整后像这样: <template> <div> <div v-for="(block, idx) in renderedBlocks" :key="idx" v-html="block" class="markdown-block" ></div> </div> </template> <script setup> import { ref, computed, watch } from 'vue' import MarkdownIt from 'markdown-it' const markdownContent = ref('') const md = new MarkdownIt() const renderedBlocks = ref([]) const blockCache = ref([]) watch( markdownContent, (newContent, oldContent) => { const blocks = newContent.split(/\n{2,}/) // 只重新渲染最后一个块,其余用缓存 // 处理块减少、块增多的场景 blockCache.value.length = blocks.length for (let i = 0; i < blocks.length; i++) { // 只渲染最后一个,或新块 if (i === blocks.length - 1 || !blockCache.value[i]) { blockCache.value[i] = md.render(blocks[i] || '') } // 其余块直接复用 } renderedBlocks.value = blockCache.value.slice() }, { immediate: true } ) onMounted(() => { // markdownContent.value = await fetch() ... }) </script> 终极武器:用 morphdom 实现精准更新 分块渲染虽然解决了大部分问题,但遇到 Markdown 列表就有点力不从心了。因为 Markdown 语法里,列表项之间通常只有一个换行符,整个列表会被当成一个大块。想象一下一个几百项的列表,哪怕只更新最后一项,整个列表块也要全部重来,前面的问题又回来了。 morphdom 是何方神圣? morphdom 是一个仅 5KB(gzip 后)的 JavaScript 库,核心功能是:接收两个 DOM 节点(或 HTML 字符串),计算出最小化的 DOM 操作,将第一个节点 “变形” 为第二个节点,而非直接替换。 其工作原理类似虚拟 DOM 的 Diff 算法,但直接操作真实 DOM: 对比新旧 DOM 的标签名、属性、文本内容等; 仅对差异部分执行增 / 删 / 改操作(如修改文本、更新属性、移动节点位置); 未变化的 DOM 节点会被完整保留,包括其事件监听、滚动位置、选中状态等。 Markdown 把列表当整体,但生成的 HTML 里,每个列表项 (<li>) 都是独立的!morphdom 在更新后面的列表项时,能保证前面的列表项纹丝不动,状态自然就保住了。 这不就是我们梦寐以求的效果吗?在 Markdown 实时更新的同时,最大程度留住用户的操作状态,还能省掉一堆不必要的 DOM 操作! 示例代码 <template> <div ref="markdownContainer" class="markdown-container"> <div id="md-root"></div> </div> </template> <script setup> import { nextTick, ref, watch } from 'vue'; import MarkdownIt from 'markdown-it'; import morphdom from 'morphdom'; const markdownContent = ref(''); const markdownContainer = ref(null); const md = new MarkdownIt(); const render = () => { if (!markdownContainer.value.querySelector('#md-root')) return; const newHtml = `<div id="md-root">` + md.render(markdownContent.value) + `</div>` morphdom(markdownContainer.value, newHtml, { childrenOnly: true }); } watch(markdownContent, () => { render() }); onMounted(async () => { // 等待 Dom 被挂载上 await nextTick() render() }) </script> 眼见为实:Demo 对比 下面这个 iframe 里放了个对比 Demo,展示了不同方案的效果差异。 小技巧: 如果你用的是 Chrome、Edge 这类 Chromium 内核的浏览器,打开开发者工具 (DevTools),找到“渲染”(Rendering) 标签页,勾选「突出显示重绘区域(Paint flashing)」。这样你就能直观看到每次更新时,哪些部分被重新绘制了——重绘区域越少,性能越好! 阶段性成果 从最开始的“暴力全量刷新”,到“聪明点的分块更新”,再到如今“精准手术刀般的 morphdom 更新”,我们一步步把那些不必要的渲染开销给砍掉了,最终搞出了一个既快又能留住用户状态的 Markdown 实时渲染方案。 不过,用 morphdom 这个第三方库来直接操作 Vue 组件里的 DOM,总觉得有点...不够“Vue”?它虽然解决了核心的性能和状态问题,但在 Vue 的世界里这么玩,多少有点旁门左道的意思。 下篇预告: 在下一篇文章里,咱们就来聊聊,在 Vue 的世界里,有没有更优雅、更“原生”的方案来搞定 Markdown 的精准更新?敬请期待!

2025/7/12
articleCard.readMore

node-sass 迁移至 dart-sass 踩坑实录

更新目标 node-sass -> sass ( dart-sass ) 减少影响面,非必要不更新其他依赖的版本 在前两条基础上,看看能否提升 node.js 的版本 抛弃 node-sass 的理由 node-sass 已经停止维护,dart-sass 是 sass 官方主推的继任者 node-sass 在 windows 下的安装非常麻烦,npm 安装时需要开发机上同时装有 python2 和 Microsoft Visual C++ 在安装 node-sass 时,需要从 Github 拉取资源,在特定网络环境下成功率并不高 项目依赖版本现状 node@^12 vue@^2 webpack@^3 vue-loader@^14 sass-loader@^7.0.3 node-sass@^4 更新思路 node.js webpack 官方并没有提供 webpack 3 支持的最高 node 版本,且即使 webpack 官方支持,webpack 的相关插件也未必支持。因此 node 版本能否更新就只能自己试。好在尽管这个项目的 CI/CD 跑在 node 12,但我日常都在用 node 14 开发,因此顺势将 node 版本提升至 14。 webpack、sass-loader webpack 的版本目前处于非必要不更新的定时炸弹状态,基于现有的 webpack 3 限制,所支持的最高 sass-loader 版本就是 ^7 ( sass-loader 在 8.0.0 版本的更新日志中明确指出 8.0.0 版本需要 webpack 4.36.0)。 如果项目中 sass-loader@^7 支持使用 dart-sass 就可以不更新 sass-loader,也就不必更新 webpack 版本;反之,就需要同步更新 webpack 至 4,再视情况定下 sass-loader 的版本。 那么到底支不支持呢?我在 webpack 官方文档介绍 sass-loader 的页面找到了这样一段 package.json 片段 { "devDependencies": { "sass-loader": "^7.2.0", "sass": "^1.22.10" } } 这证明起码在 sass-loader@7.2.0 这一版本就已经支持 dart-sass 了,因此 webpack 版本可以停留在 ^3,而 sass-loader 暂时停留在 7.0.3 版本,如果后续有问题可以更新到 ^7 版本中最新的 7.3.1 版本。 dart-sass sass-loader@^7 所支持的最高 sass 我并没有查到,Github Copilot 信誓旦旦地告诉我 官方文档引用: sass-loader@^7.0.0 requires node-sass >=4.0.0 or sass >=1.3.0, <=1.26.5. 建议: 如果需要使用更高版本的 sass,请升级到 sass-loader 8 或更高版本。 但事实上,我并没有在互联网上找到这段文本的蛛丝马迹。并且在 sass 的 ~1.26 版本中最后一个版本是 1.26.11 而非 1.26.5,根据常见的 npm 版本号原则,major version 和 minor version 不变,只改变了 patch version 的发版一般只有 bugfix 而没有 breaking change,不至于从 1.26.5 更新到 1.26.11 就突然不支持 sass-loader 7 了,因此更可能是 AI 幻觉或者是训练数据受限。 出于谨慎考虑,最终决定采用 webpack 官方文档中提到的 sass 1.22 的最后一个版本,也就是 1.22.12。 分析完成,动手更新 第一步,卸载 node-sass,安装 sass@^1.22.12 npm uninstall node-sass npm install sass@^1.22.12 第二步,更新 webpack 配置(非必须) module.exports = { // ... module: { rules: [ { test: /\.(scss|sass)$/, use: [ 'style-loader', 'css-loader', { loader: 'sass-loader', + options: { + // 事实上,这一行在大部分 sass-loader 版本中不用加,sass-loader 能自动检测本地是 sass 还是 node-sass + implementation: require('sass') + }, }, }, ], }, ], }, }; 第三步,批量替换 /deep/ 语法为 ::v-deep 因为 /deep/ 写法在 2017 年被弃用 ,/deep/ 变成了不受支持的深度作用选择器,node-sass 凭借其出色的容错性能够继续提供兼容,但 dart-sass 则不支持这种写法。于是需要将 /deep/ 语法批量替换成 ::v-deep 写法,这种写法虽然在 vue 的后续 rfc 被放弃了,但直至今日依然在事实上被支持。 # 大概就是这么个意思,用 vscode 的批量替换其实也行 sed -i 's#\s*/deep/\s*# ::v-deep #g' $(grep -rl '/deep/' .) 第四步,修复其他 sass 语法错误 在迁移的过程中,我发现项目中有一些不规范的写法,node-sass 凭借出色的鲁棒性不吭一声强行解析,而 dart-sass 则干不了这粗活。因此需要根据编译时的报错手动修复一下这些语法错误,我这里一共遇到两种。 // 多打了一个冒号 .foo { - color:: #fff; + color: #fff; } // :nth-last-child 没指定数字 .bar { - &:nth-last-child() { + &:nth-last-child(1) { margin-bottom: 0; } } 踩坑 ::v-deep 样式不生效 依赖更新完后看了两眼好像是没问题,就推测试环境了。结果一天没到就被同事 call 了,::v-deep 这种深度作用选择器居然没有生效? 抱着试一试的态度,GPT 给了如下回答 在 Vue 2 + vue-loader + Sass 的组合下,这种写法是正确的,前提是你的构建工具链支持 ::v-deep 语法(如 vue-loader@15 及以上版本 + sass-loader)。 虽说我依然没有查证到为什么更新 vue-loader@15 才能使用 ::v-deep 语法,但对 vue-loader 进行更新后,::v-deep 语法确实生效了。在撰写本文时,我找到了些许蛛丝马迹,可能能解释这一问题。 vue-loader 在 14 版本的官方文档就是没有 ::v-deep 写法的示例,这一示例一直在 vue-loader 15.7.0 版本发布后才被加入。 vue-cli 的 Github Issue 评论区中有人提到 ::v-deep implemented in @vue/component-compiler-utils v2.6.0, should work after you reinstall the deps. 而 vue-loader 在 15.0.0-beta.1 版本才将 @vue/component-compiler-utils 加入到自己的 dependencies 中,并直到 vue-loader 15.7.1 中才将其 @vue/component-compiler-utils 的版本号更新到满足要求的 ^3.0.0 那能否升级到 vue-loader 16 甚至 17 版本呢?不行,在 vue-loader v16.1.2 的更新日志中明确写道 Note: vue-loader v16 is for Vue 3 only. vue-loader 14 -> 15 breaking change vue-loader 从 14 往上迁移时,不修改 webpack 配置直接跑会遇到 vue 语法不识别的问题。具体表现为 .vue 文件命名都是正确有效的语法,但构建开发时编译器就是不认,报语法错误。vue-loader 官方有一份迁移文档,需要注意一下。 ERROR in ./src/...... Module parse failed: Unexpected token(1:0) You may need an appropriate loader to handle this file type. // ... import path from 'path' +const VueLoaderPlugin = require('vue-loader/lib/plugin') // ... plugins: [ + new VueLoaderPlugin() // ... ] 除此之外,在我这个项目中需要额外移除 webpack 配置中针对 .vue 文件的 babel-loader { test: /\.vue$/, use: [ - { - loader: 'babel-loader' - }, { loader: 'vue-loader', } ] } 最终更新情况 node@^12 -> node@^14 vue-loader@^14 -> vue-loader@^15 node-sass@^4 -> sass@^1.22.12 其余依赖版本维持不变 参见 node-sass更换为dart-sassdart-sass 和 node-sass都是用来将sass编译成 - 掘金 node-sass迁移dart-sass | Bolg sass-loader | webpack 中文文档 | webpack中文文档 | webpack中文网 Sass: LibSass is Deprecated sass - npm node-sass - npm About semantic versioning | npm Docs Make /deep/ behave like the descendant combinator " " in CSS live profile (in css file or inside of <style>) - Chrome Platform Status sass-loader/CHANGELOG.md at v8.0.0 · webpack-contrib/sass-loader Release v16.1.2 · vuejs/vue-loader refactor: use @vue/component-compiler-utils · vuejs/vue-loader@e32cd0e chore: update @vue/component-compiler-utils to v3 · vuejs/vue-loader@c359a38 dart-sass does not support /deep/ selector · Issue #3399 · vuejs/vue-cli Scoped CSS · vue-loader v14 Migrating from v14 | Vue Loader

2025/7/5
articleCard.readMore

前端中的量子力学——一打开 F12 就消失的 Bug

前端「量子态」现象的首次观测 这事说来也邪乎,半个月前吃着火锅唱着歌,在工位上嘎嘎写码,发现一个诡异的 bug。作为如假包换的人类程序员,写出 bug 是再正常不过的事情了,但这 bug 邪门就邪门在我一打开 F12 的 DevTools 观察相关的 dom 结构,这 bug 就自动消失了;再把 DevTools 一关,Ctrl + F5 一刷新页面,Bug 又出现了。 下面是使用 iframe 引入的 demo 这 Bug 给我整得脑瓜子嗡嗡的,我又不是物理学家,写个前端怎么量子力学的观察者效应都给我整出来了(? 观测者效应(Observer effect),是指“观测”这种行为对被观测对象造成一定影响的效应。 在量子力学实验中,如果要测算一个电子所处的速度,就要用两个光子隔一段时间去撞击这个电子,但第一个光子就已经把这个电子撞飞了,便改变了电子的原有速度,我们便无法测出真正准确的速度(不确定原理)。时间流逝的快慢也会受到观测者的影响,用很高的频率去观测粒子的衰变,反而使得粒子长时间不衰变。 ——wikipedia 量子迷雾❌浏览器机制✅ 这里先稍微解释一下 demo 中的代码片段: if (scrollIndex >= groupLength) { setTimeout(() => { wrapper.style.transition = "none"; scrollIndex = 0; wrapper.style.transform = `translateY(-${crollIndex * itemHeight}px)`; requestAnimationFrame(() => { wrapper.style.transition = "transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1)"; }); }, 500); } 我这边拿到的需求是需要写一个无限滚动的轮播标题列表,每次展示三个,2 秒后标题列表整体上移,原本的第一个标题就移出可视范围了,下面会新补充一个新的标题列表。(可能解释的不清楚,但各位应该都看过上面的 demo 了) 当列表滚动到最底部的时候,我先取消 transition 过渡动画效果,趁机将整体列表平移到上一次可视范围内出现相同的三个标题的位置,再把 transition 过渡动画的效果加回来,这样就能在视觉上造成无限滚动的效果。 但问题就出在明明把 transition 属性取消了,但这一次平移仍然触发了过渡动画效果。 说实话,这是我短暂的码农生涯当中最绝望的一次,一方面是遇到的 bug 过于逆天以至于说出去都可能没人信,一方面是遇上这种问题是完全不能指望搜索引擎能给出什么解决方案的——毕竟我自己都不知道该怎么组织关键词进行搜索。 于是抱着试一试的心态,把相关代码喂给 ChatGPT-4o 看看能不能问出个所以然来。 你描述的现象——“滚动第九次时列表出现突兀的自下而上跳动,而打开 F12 时不会出现问题”——几乎可以确定是由于浏览器在某些渲染状态下跳过了某些帧(帧率波动)或者定时器精度的问题导致动画突变。 这种问题多半发生在“使用 setInterval 控制动画”和“切换样式(transition)时机不当”所引起的 过渡跳帧问题,而打开 DevTools 会 强制刷新帧或提高定时器精度,从而掩盖了这个问题。 太好了,是 requestAnimationFrame,我们有救了 window.requestAnimationFrame() 方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。 ——MDN 这是 GPT 给出的方案,非常有效 if (scrollIndex >= groupLength) { setTimeout(() => { wrapper.style.transition = "none"; scrollIndex = 0; wrapper.style.transform = `translateY(-${crollIndex * itemHeight}px)`; requestAnimationFrame(() => { + requestAnimationFrame(() => { wrapper.style.transition = "transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1)"; + }); }); }, 500); } 如果觉得嵌套两层 requestAnimationFrame 比较难理解,那下面的代码是等效的 if (scrollIndex >= groupLength) { setTimeout(() => { scrollIndex = 0; requestAnimationFrame(() => { // 第一帧 wrapper.style.transition = "none"; wrapper.style.transform = `translateY(-${crollIndex * itemHeight}px)`; // 第二帧 requestAnimationFrame(() => { wrapper.style.transition = "transform 0.5s cubic-bezier(0.25, 0.1, 0.25, 1)"; }); }); }, 500); } 总之,我们需要杜绝浏览器将设置 transform 偏移值(瞬移列表位置)与恢复 transition 动画两件事合并到同一帧里去,而两层嵌套的 requestAnimationFrame 方法能很好的解决这个问题 驯服量子态:前端开发者的新技能 就这样,通过使用两层requestAnimationFrame,我们成功驯服了这个"量子态"的bug。现在无论是否打开F12,它都会乖乖地按照我们的预期滚动,不再玩消失的把戏。 看来,在前端的世界里,我们不仅要懂JavaScript,还得懂点量子力学。下次再遇到这种"一观测就消失"的bug,不妨试试这个"量子纠缠解决方案"——双重requestAnimationFrame,没准就能让bug从"量子态"坍缩成"稳定态"呢! 当然,如果你有更神奇的 debug 经历,欢迎分享你的经历——毕竟,在代码的宇宙里,我们永远不知道下一个bug会以怎样的形态出现。也许,这就是编程的乐趣(?)所在吧! 本文由 ChatGPT 与 DeepSeek 协助撰写,但 bug 是真人真事(泪)。 参见 观测者效应 - 维基百科,自由的百科全书 Window:requestAnimationFrame() 方法 - Web API | MDN 网页性能管理详解 - 阮一峰的网络日志

2025/6/8
articleCard.readMore

2025 年,如何为 web 页面上展示的视频选择合适的压缩算法?

事情的起因是需要在网页上展示一个时长约为 5 分钟的产品展示视频,拿到的 H264 编码的原文件有 60MB 大。高达 1646 Kbps 码率的视频文件通过网络传输,烧 cdn 流量费用不说,对于弱网环境下的用户体验也绝对不会好。因此必须在兼顾浏览器兼容性(太好了不用管 IE)的情况下,使用更现代的视频压缩算法进行压缩。 哪些压缩算法是目前的主流? AV1 AV1 作为目前压缩效率最高的主流视频编码格式,在 2025 年的今天已经在 YouTube、Netflix、Bilibili 等视频网站全面铺开,毫无疑问是最值得优先考虑的选择;除了优异的压缩效率以外,AV1 免版税的优势使得各硬件厂商和浏览器内核开发者可以无所顾忌的将 AV1 编码的支持添加到自己的产品中。 可惜的是,Safari 并没有对 AV1 编码添加软解支持,只有在搭载 Apple M3 及后续生产的 Mac 和 iPhone 15 Pro 后续的机型才拥有硬解 AV1 的能力,在此之前生产的产品均无法使用 Safari 播放 AV1 编码的视频。我宣布 Safari 已经成为当代 IE,妥妥阻碍 Web 发展的绊脚石 除此之外,AV1 在压制视频时对设备的要求较高。在桌面端的消费级显卡中,目前只有 NVIDIA RTX 40 系、AMD Radeon RX 7000 系、IntelArc A380 及后续的产品拥有 AV1 的编码(encode)支持。而 Apple M 系列芯片至今没有任何一款产品拥有对 AV1 编码的硬件支持。这也导致我在我搭载 Intel Core i7-1165G7 的 ThinkPad 上使用 AV1 编码压缩视频时被迫使用 libaom-av1 进行软件编码,1080p 的视频压缩效率为 0.0025x 的速率,五分钟的视频要压一天多的时间。 H.265 / HEVC 作为 H.264 / AVC 的下一代继任者,H.265(又称 HEVC)的表现可谓是一手好牌打得稀巴烂。HEVC 由多个专利池(如 MPEG LA、HEVC Advance 和 Velos Media)管理,授权费用高且分散,昂贵的专利授权费用严重限制了它的普及速度和范围,尤其是在开放生态和网页端应用中。 Chromium / Firefox 不愿意当承担专利授权费的冤大头,拒绝在当今世界最大的两个开源浏览器内核中添加默认的 H.265 软解支持,目前主流浏览器普遍采用能硬解就硬解,硬解不了就摆烂的支持策略。Firefox on Linux 倒是另辟蹊径,不仅会尝试使用硬解,还会尝试使用用户在电脑上装的 ffmpeg 软解曲线救国。不过好在毕竟是 2013 年就确定的标准,现在大部分硬件厂商都集体被摁着脖子交了专利授权费以保证产品竞争力,Apple 更是 HEVC 的一等公民,保证了全系产品的 HEVC 解码能力。 目前未覆盖到的场景主要是 Chromium / Firefox on Windows 7 和 Chromium on Linux(包括 UOS、麒麟等一众国产 Linux 发行版)。 VP9 VP9 是 Google 于 2013 年推出的视频编码格式,作为 H.264 的继任者之一,在压缩效率上接近 H.265(HEVC),但最大的杀手锏是——彻底免专利费。这也让 VP9 成为 Google 对 HEVC 高额授权费用的掀桌式回应:你们慢慢吃,我开一桌免费的。 借着免专利的东风和 Google 自家产品矩阵的强推,VP9 在 YouTube、WebRTC 乃至 Chrome 浏览器中迅速站稳了脚跟。特别是在 AV1 普及之前,VP9 几乎是网页视频播放领域的事实标准,甚至逼得苹果这个“编解码俱乐部元老”在 macOS 11 Big Sur 和 iOS 14 上的 Safari 破天荒地加入了 VP9 支持(尽管 VP9 in webm 的支持稍晚一些,具体见上表)。 VP9 的软解码支持基本无死角:Chromium、Firefox、Edge 都原生支持,Safari 也一反常态地“从了”。硬件解码方面,从 Intel Skylake(第六代酷睿)开始,NVIDIA GTX 950 及以上、AMD Vega 和 RDNA 系显卡基本都具备完整的 VP9 解码能力——总之,只要不是博物馆级别的老电脑,就能愉快播放 VP9 视频。 当然,编码仍是 VP9 的短板。Google 官方提供的开源实现 libvpx,速度比不上 x264/x265 等老牌选手,在缺乏硬件加速的场景下,仍然属于“关机前压一宿”的那种体验。不过相比 AV1 的 libaom-av1,VP9 至少还能算“可用”,适合轻量化应用、实时通信或是对压制速度敏感的用户,而早在 7 代 Intel 的 Kaby Lake 系列产品就已经引入了 VP9 的硬件编码支持,各家硬件厂商对 VP9 硬件编码的支持发展到今天还算不错。 H.264 / AVC 作为“老将出马一个顶俩”的代表,H.264 / AVC 无疑是过去二十年视频编码领域的霸主。自 2003 年标准确定以来,凭借良好的压缩效率、广泛的硬件支持和相对合理的专利授权策略,H.264 迅速成为从网络视频、蓝光光盘到直播、监控乃至手机录像的默认选择。如果你打开一个视频网站的视频流、下载一个在线视频、剪辑一个 vlog,大概率都绕不开 H.264 的身影。 H.264 的最大优势在于——兼容性无敌。不夸张地说,只要是带屏幕的设备,就能播放 H.264 视频。软解?早在十几年前的浏览器和媒体播放器中就已普及;硬解?从 Intel Sandy Bridge、NVIDIA Fermi、AMD VLIW4 这些“史前”架构开始就已加入对 H.264 的完整支持——你甚至可以在树莓派、智能冰箱上流畅播放 H.264 视频。 虽然 H.264 同样存在和 H.265 相同的专利问题,但其授权策略明显更温和——MPEG LA 提供的专利池授权门槛较低,且不向免费网络视频收取费用,使得包括 Chromium、Firefox 在内的浏览器都默认集成了 H.264 的软解功能。Apple 和 Microsoft 更是早早将其作为视频编码和解码的第一公民,Safari 和 Edge 天生支持 H.264,不存在任何兼容性烦恼。 当然,作为一项 20 多年前的技术,H.264 在压缩效率上已经明显落后于 VP9、HEVC 和 AV1。相同画质下,H.264 的码率要比 AV1 高出 30~50%,在追求极致带宽利用或存储节省的应用场景中就显得有些力不从心。然而在今天这个“能播比好看更重要”的现实环境中,H.264 依然是默认方案,是“稳健老哥”的代名词。 所以,即便 AV1、HEVC、VP9 各有亮点,H.264 依旧凭借“老、稳、全”三大核心竞争力,在 2025 年依然牢牢占据着视频生态链的中枢地位——只要这个世界还有浏览器不支持 AV1(可恶的 Safari 不支持软解),服务器不想烧钱转码视频,或用户设备太老,H.264 就不会退场。 小结 在视频编码方面,浏览器不再是那个能靠一己之力抹平硬件和系统差异的超人,所以总有一些特殊情况是表格中无法涵盖的。 编解码器 压缩效率 浏览器 桌面端支持 移动端支持 备注 AV1 ★★★ Chrome / Chromium 是 (v70+,发布于 2018 年 10 月) 是 (v70+,发布于 2018 年 10 月) 硬解优先,软解后备 Firefox 是 (v67+,发布于 2019 年 5 月) 是 (v113+,发布于 2023 年 5 月) 硬解优先,软解后备 Safari 不完全支持 (仅近两年的产品支持) 不完全支持 (仅近两年的产品支持) 仅支持硬解 (M3, A17 Pro 系芯片后开始支持),无软解支持 HEVC (H.265) ★★☆ Chrome / Chromium 不完全支持 不完全支持 仅支持硬解,无软解支持(Windows 可从微软商店安装付费的软解插件) Firefox 不完全支持 不完全支持 仅支持硬解,无软解支持(Linux 可依赖系统 ffmpeg 实现软解) Safari 近期设备全部支持 (macOS High Sierra+,发布于 2017 年 6 月) 近期设备全部支持 (iOS 11+,发布于 2017 年 10 月) 苹果是 H.265 一等公民 VP9 ★★☆ Chrome / Chromium 是 是 支持良好 Firefox 是 是 支持良好 Safari 是 (v14.1+,发布于 2021 年 4 月) 是 (iOS 17.4+,发布于 2024 年 3 月) 支持稍晚(此处指兼容 vp9 的 webm 时间,vp9 in WebRTC 的兼容时间更早) H.264 (AVC) ★☆☆ Chrome / Chromium 是 是 通用 Firefox 是 是 通用 Safari 是 是 通用 怎么选? 我们不是专业的视频托管平台,不像 YouTube、Bilibili 那样专业到可以向用户提供多种分辨率、压缩算法的选择。 最终的选择策略,必须在压缩效率、播放兼容性、编码耗时等维度之间做出权衡。 选择一:AV1 挑大梁,H.264 保兼容 现代浏览器支持在 <video> 标签中使用 <source> 标签和 MIME type 让浏览器按需播放 <video controls poster="preview.jpg"> <source src="video.av1.webm" type='video/webm; codecs="av01"' /> <source src="video.h264.mp4" type='video/mp4' /> 当前浏览器不支持视频播放 </video> 通过这样的写法,浏览器会自动选择最先能解码的 source,无需写复杂的判断逻辑或使用 JavaScript 动态切换。默认的 AV1 编码在最大程度上减少了传输流量降低成本,享受现代浏览器与设备的压缩红利;而 H.264 则作为兜底方案,保证了在不支持 AV1 的 Safari 等老旧设备上的回放兼容性。 然而这个选择可能并不是太合适,一方面我手上最先进的处理器 Apple M4 并不支持硬件编码 AV1 视频,5 分钟的视频压完需要整整 3 个小时,如果还需要视压缩质量来回调整压缩参数重新压上几次,那可真是遭老罪了;另一方面,即使 Chromium / Firefox 等主流浏览器内核现在都支持 AV1 的软解,但在一些硬件较老的设备上播放 AV1 编码的视频可能让用户的电脑风扇原地起飞,这一点在 YouTube 大力推广 AV1 的时候就曾遭到不少用户的诟病。 选择二:VP9 独挑大梁 考虑到 AV1 编码的高昂成本和用户电脑风扇原地起飞的风险,VP9 也是一个非常具有竞争力的选择。VP9 在主流浏览器中得到了非常好的兼容,因此可以考虑放弃 H.264 的 fallback 方案独挑大梁。而 VP9 硬件编码在近几年的硬件设备上的普遍支持也给足了我勇气,让我可以多次调整压缩质量重新压缩,找一个在文件体积和画面清晰度之间的 sweet point。 由于是 VP9 独挑大梁,因此大多数人可能会考虑使用与 VP9 最为适配的 webm 格式封装视频。但目前在 webm 中最广泛使用的音频编码 opus 在 Safari 上的兼容性并不是太好(在 2024 年 3 月发布的 Safari 17.4 才开始支持),建议斟酌一下是不是继续用回 AAC 编码,并将视频封装在 mp4 中。 音频码率太高?再砍一刀 上面说了那么多的视频压缩算法,其实只是局限于视频画面的压缩,音频这一块其实还能再压一点出来。 Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 128 kb/s (default) 一个介绍产品的视频,在音频部分采用了 48000 Hz 双声道采样,码率高达 128 kbps,说实话有点奢侈。我直接砍成 64 kbps 单声道,又省下 2MB 的文件大小。 写在最后 对于前端开发者来说,视频压缩算法的选择早已不是单纯的“压得小不小”问题,而是一场在设备能力、浏览器兼容性、用户体验与开发成本之间的博弈。我们既要跟上技术演进的节奏,拥抱 AV1、VP9 等更高效的编解码器,也要在实际项目中照顾到现实中的设备分布和播放环境。 在理想与落地之间,我们所能做的,就是充分利用 HTML5 提供的容错机制,搭配好合适的编码策略和封装格式,让网页上的每一段视频都能在合适的设备上、以合理的代价播放出来。 毕竟,Web 从来不缺“能不能做”,缺的是“做得优雅”。如果说编码器是硬件工程师和视频平台的战场,那 <video> 标签下的这几行 <source>,才是属于我们前端工程师的战壕。 参见 网页视频编码指南 - Web 媒体技术 | MDN Encoding & Quality - Netflix Research How the VP9 Codec Supports Now Streaming to Apple Devices & More | dolby.io Audio/Video | The Chromium Project AV1 video format | Can I use... Support tables for HTML5, CSS3, etc WebM video format | Can I use... Support tables for HTML5, CSS3, etc HEVC/H.265 video format | Can I use... Support tables for HTML5, CSS3, etc Opus audio format | Can I use... Support tables for HTML5, CSS3, etc MPEG-4/H.264 video format | Can I use... Support tables for HTML5, CSS3, etc AV1 - Wikipedia High Efficiency Video Coding - Wikipedia VP9 - Wikipedia Advanced Video Coding - Wikipedia Encode and Decode Capabilities for 7th Generation Intel® Core™... macOS High Sierra - 维基百科,自由的百科全书 Chrome 70 adds AV1 video support, improves PWAs on Windows, and more [APK Download] Firefox for Android 113.0, See All New Features, Updates and Fixes 视频网站的“蓝光”是怎么骗你的?——视频画质全解析【柴知道】_哔哩哔哩_bilibili 《4K 清晰度不如4年前,视频变糊是你的错觉吗》- 原视频已 404

2025/6/2
articleCard.readMore

el-image 和 el-table 怎么就打架了?Stacking Context 是什么?

这是精弘内部的图床开发时遇到的事情,大一的小朋友反馈说 el-image 和 el-table 打架了。 demo 的 iframe 引入 看到后面的表格透出 el-image 的预览层,我的第一反应是叫小朋友去检查 z-index 是否正确,el-image 的 mask 遮罩的 z-index 是否大于表格。 经过我本地调试,发现 z-index 的设置确实没问题,但后面的元素为什么会透出来?谷歌搜索一番,找到了这篇文章 给 el-table 加一行如下代码即可 .el-table__cell { position: static !important; } 经本地调试确认,这一方案确实能解决问题,但为什么呢?这就涉及到 Stacking Context (层叠上下文)了。 Stacking Context(层叠上下文)究竟是什么? 简单来说,Stacking Context 可以被类比成画布。在同一块画布上,z-index 值越高的元素就处于越上方,会覆盖掉 z-index 较低的元素,这也是为什么我最开始让检查 z-index 的设置是否有问题。但问题出在 Stacking Context 也是有上下顺序之分的。 现在假设我们有 A、B 两块画布,在 A 上有一个设置了 z-index 为 1145141919810 的元素。那这个元素具备非常高的优先级,理应出现在浏览器窗口的最上方。但如果 B 画布的优先级高于 A 画布,那么 B 元素上的所有元素都会优先显示(当了躺赢狗)。那么画布靠什么来决定优先级呢? 处于同级的 Stacking Context 之间靠 z-index 值来区分优先级 对于 z-index 值相同的 Stacking Context,在 html 文档中位置靠后的元素拥有更高的优先级 第二条规则也能解释为什么在上面的 demo 中,只有在表格中位置排在图片元素后面的元素出现了透出来的情况。 所以为什么 el-image 和 el-table 打架了? 这次的冲突主要是下面两个因素引起的 el-table 给每个 cell 都设置了 position: relative 的 css 属性,而 position 被设为 relative 时,当前元素就会生成一个 Stacking Context。 所以我们这么一个有十个格子的表格,其实就生成了十个画布。而这其中每个画布 z-index 都为 1。根据刚才的规则,在图片格子后面的那些格子对应的 html 代码片段在整体的 html 文档中更靠后,所以他们的优先级都高于图片格子。 el-image 的预览功能所展开的遮罩层处于 el-image 标签内部 上图中橙色部分是 el-image 在预览时提供的遮罩,可以看到 element-plus 组件的 image 预览的默认行为是将预览时所需要的遮罩层直接放在 <el-image> </el-image> 标签内部,这导致 el-image 的遮罩层被困在一个低优先级的 Stacking Context 中,后面的格子里的内容就是能凭借高优先级透过来。 所以解决方案是什么? 更改 position 值在这里确实是可行的 上面我谷歌搜到的将 el-table 中 cell 的 position 值强制设为 static 确实是有效的,因为 static 不会创建新的 Stacking Context,这样就不会有现在的问题。 将需要出现在最顶层的代码放置在优先级最大的位置是更常见的方案 但别的组件库在处理这个需求时,一般会将预览时提供的遮罩的 html 代码片段直接插入到 body 标签内部的最尾部,并设置一个相对比较大的 z-index 值,以确保这个遮罩层能够获得最高的优先级,以此能出现在屏幕的最上方。(像一些 dialog 对话框、popover 悬浮框也都是这个原理)。 事实上,element-plus 组件库也提供了这个功能 preview-teleported: image-viewer 是否插入至 body 元素上。嵌套的父元素属性会发生修改时应该将此属性设置为 true 所以在使用 el-image 时传入一个 :preview-teleported="true" 是一个更普适的方案,因为我们并不能确保 el-image 的父元素除了 el-table 的 cell 以外还有什么其他的父元素会创建新的 Stacking Context。 参见 层叠上下文 - CSS:层叠样式表 | MDN 彻底搞懂CSS层叠上下文、层叠等级、层叠顺序、z-index最近,在项目中遇到一个关于CSS中元素z-index属性的问 - 掘金 深入理解CSS中的层叠上下文和层叠顺序 « 张鑫旭-鑫空间-鑫生活 Image 图片 | Element Plus element ui e-image 和e-table一起使用显示问题_el-table el-image-CSDN博客

2025/5/31
articleCard.readMore

2025年,前端如何使用 JS 将文本复制到剪切板?

基础原理 如果你尝试在搜索引擎上检索本文的标题,你搜到的文章大概会让你使用下面两个 API。我希望你用的搜索引擎不至于像某度一样灵车到 2025 年还在让你使用基于 Flash 的 ZeroClipboard 方案 document.execCommand 2012 年不止有世界末日,还有 IE 10。随着 IE 10 在当年 9 月 4 日发布,execCommand 家族迎来了两个新的成员—— copy/cut 命令(此说法来自 Chrome 的博客,而 MDN 认为 IE 9 就已经支持了)。三年之后,随着 Google Chrome 在 2015 年 4 月 14 日的发布的 42 版本对 execCommand 的 copy/cut 跟进,越来越多的浏览器厂商开始在自家的浏览器中跟进这个实现标准。最终在 2016 年 9 月 13 日发布的 Safari 10 on IOS 后,WEB 开发者们总算获得了历史上第一个非 Flash 实现的 js 复制到剪切板的方案。 当 document.execCommand 的第一个参数为 copy 时,可以将用户选中的文本复制到剪切板。基于这个 API 实现,很快便有人研究出了当今 web 下最常见的 js 实现——先创建一个不可见的 dom,用 js 操作模拟用户选中文本,并调用 execCommand('copy') 将文本复制到用户的剪切板。大致的代码实现如下: // 来自「JS复制文字到剪贴板的坑及完整方案。」一文,本文结尾有跳转链接 const textArea = document.createElement("textArea"); textArea.value = val; textArea.style.width = 0; textArea.style.position = "fixed"; textArea.style.left = "-999px"; textArea.style.top = "10px"; textArea.setAttribute("readonly", "readonly"); document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); 尽管这个 API 早已被 w3c 弃用,在 MDN 被标注为 Deprecated,但这仍然是市面上最常见的方案。在编写本文的时候,我扒了扒 MDN 的英文原始页面在 archive.org 的存档及其在 Github 的变更记录,这个 API 在 2020 年 1~2 月被首次标记为 Obsolete(过时的),在 2021 年 1 月被首次标记为 Deprecated(已弃用),并附上了红色 Section Background Color 提示开发者该 API 可能随时无法正常工作。但截至本文发布,所有的常用浏览器都保留着对该 API 的兼容,起码在 copy 命令下是这样的。 这个 API 被广泛应用在了太多站点,以至于移除对该 API 的支持将会导致大量的站点异常,我想各家浏览器内核在短期内恐怕都没有动力以丢失兼容性为代价去移除这个 API,这也意味着这个创建一个不可见的 dom 代替用户选中文本并执行 execCommand 复制到用户剪切板的(看似奇葩的)曲线救国方案已然在前端开发的历史上留下了浓墨重彩的一笔。 Clipboard.writeText() 随着原生 JS 一步步被增强,开发者们总算补上了 Clipboard 这一块的拼图。2018 年 4 月 17 日,Chrome 66 率先迈出了这一步;同年 10 月 23 日,Firefox 跟进了 ClipBoard API 的实现。最终在 2020 年 3 月 24 日,随着 Apple 自家 Safari 13.4 的姗姗来迟,前端开发者门总算喘了口气,再一次得到了一个主流浏览器通用的复制方案。 那么 execCommand 明明已经实现了纯 js 实现的复制文本到剪切板了,为什么我们还需要 Clipboard API ?或者说,这个特意去实现的 Clipboard API 到底有什么优势? 传统的 execCommand 方案在使用的时候通常需要创建一个临时的不可见的 DOM,放入文本、用 JS 选中文本、执行 copy 命令。我们暂且不说这种 hacky 的方式在代码编写时是多么不优雅,但一个使用 JS 去选中文本这个操作就会修改用户当前的文本选择状态,在某些时候导致一些用户体验的下降。 Clipboard API 是异步的,这意味着其在复制大量文本时不会阻塞主线程。 Clipboard API 提供了更多的能力,比如 write() 和 read() 允许对剪切板读写更复杂的数据,比如富文本或图片。 Clipboard API 具有更现代、更明确的权限控制—— write 操作需要由用户的主动操作来调用,read 操作则需要用户在浏览器 UI 上明确授予权限。这些权限控制给予了用户更大的控制权,因此,当 execCommand 退出历史的舞台后,WEB 的安全性将得到进一步提升。 不过在现阶段,Clipboard.writeText() 未必就能解决所有的问题。抛开旧版浏览器的兼容性问题不谈,navigator.clipboard 仅在通过 https 访问的页面中可用(或是 localhost),如果你的项目部署在局域网,你试图通过 192.18.1.x 的 ip + port 直接访问,那么 navigator.clipboard 将会是 undefined 状态。 除此之外,安卓原生的 Webview 还有因为 Permissions API 没实现而用不了 Clipboard API 的问题。 基于以上原因,很多网站现在都会优先尝试使用 navigator.clipboard.writeText(),失败后再转去使用 execCommand('copy')。大致的代码实现如下: // 来自「JS复制文字到剪贴板的坑及完整方案。」一文,本文结尾有跳转链接 const copyText = async val => { if (navigator.clipboard && navigator.permissions) { await navigator.clipboard.writeText(val); } else { const textArea = document.createElement("textArea"); textArea.value = val; textArea.style.width = 0; textArea.style.position = "fixed"; textArea.style.left = "-999px"; textArea.style.top = "10px"; textArea.setAttribute("readonly", "readonly"); document.body.appendChild(textArea); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); } }; Flash 方案(ZeroClipboard) 其实上面两个 API 差不多就把基础原理讲完了,不过我在查资料的时候发现,在 execCommand 方案之前,前端居然大多是依靠 Flash 来实现复制文本到剪切板的,这不得拿出来讲讲? 目前在 ZeroClipboard 的 Github 仓库能找到的最老的 tag 是 v1.0.7,发布于 2012 年 6 月 9 日。我打赌这个项目不是第一个通过 Flash 实现复制文本到剪切板的,在此之前肯定有人使用 Flash 实现过这个功能,只是没单独拎出来作为一个库开源出来。 ZeroClipboard 通过创建一个透明的 Flash Movie 覆盖在触发按钮上,当用户点击按钮时,实际上点到的是 Flash Movie,随后 JavaScript 与 Flash Movie 通过 ExternalInterface 进行通信,将需要复制的文本传递给 Flash,再经 Flash 的 API 将文本写入用户的剪切板。 在当时的时代背景下,这是唯一一个能够跨浏览器实现复制文本到剪切板的方案(尽管并不是每台电脑都装有 Flash,尽管 IOS 并不支持 Flash),6.6k star 的 Github 仓库见证了那个各家浏览器抱着各家私有 API 的混沌时代,最终随着 execCommand 方案的崛起,ZeroClipboard 与 Flash 一同落幕。 其他不完美的方案 window.clipboardData.setData 该 API 主要在 2000 年 —2010 年前后被使用,仅适用于 IE 浏览器。Firefox 在这段时间里还不支持纯 js 实现的复制文本至浏览器的操作;Chrome 第一个版本在 2008 年才发布,尚未成为主流。 window.clipboardData.setData("Text", text2copy); 摆烂(prompt) 调 prompt 弹窗让用户自己复制。 prompt('Press Ctrl + C, then Enter to copy to clipboard','copy me') 第三方库封装 由于 execCommand 的方案过于抽象,不够优雅,所以我们有一些现成的第三方库对复制到剪切板的代码进行了封装。 clipboard.js clipboard.js 是最负盛名的一款第三方库,截至本文完成时间,在 Github 共收获 34.1k 的 star。最早的一个 tag 版本发布于 2015 年 10 月 28 日,也就是 Firefox 支持 execCommand、PC 端三大浏览器巨头全面兼容的一个月后。 clipboard.js 仅使用 execCommand 实现复制到剪切板的操作,项目的 owner 希望开发者自行使用 ClipboardJS.isSupported() 来判断用户的浏览器是否支持 execCommand 方案,并根据命令执行的返回值自行安排成功/失败后的动作。。 不过让我感到奇怪的是,clipboard.js 在实例化时会要求开发者传入一个 DOM 选择(或者是 HTML 元素/元素列表)。它一定要有一个实体的 html 元素,用设置事件监听器来触发复制操作,而不是提供一个 js 函数让开发者来调用——尽管这不是来自 execCommand 的限制。示例如下 <!-- Target --> <input id="foo" value="text2copy" /> <!-- Trigger --> <button class="btn" data-clipboard-target="#foo"></button> <script> new ClipboardJS('.btn'); </script> 对,就一行 js 就能给所有带有 btn class 的 dom 加上监听器。或许这就是为什么这个仓库能获得 34.1k star 的原因,在 2015 年那个大多数人还在用三件套写前端的时代,clipboard.js 能够降低代码量,不用开发者自行设置监听器。 clipboard.js 当然也提供了很多高级选项来满足不同开发者的需求,比如允许你通过传入一个 function 来获取你需要让用户复制的文本而,或是通过 Event 监听器来反馈是否复制成功,总之灵活性是够用的。 copy-to-clipboard 同样是一款利用 execCommand 的第三方库,虽然只有 1.3k star。第一个 tag 版本发布于 2015 年的 5 月 24 日,比 clipboard.js 还要早。相比起 clipboard.js,copy-to-clipboard 不依赖 html 元素,可以直接在 js 中被调用,我个人是比较喜欢这个的。在 vue/react 等现代化的前端框架中,我们一般不直接操作 dom,因此并不是很适合使用 clipboard.js,这个 copy-to-clipboard 就挺好的。此外,除了 execCommand 与方案,copy-to-clipboard 还对老版本的 IE 浏览器针对性的适配了 window.clipboardData.setData 的方案,并且在两者都失败时会调用 prompt 窗口让用户自主复制实现最终的兜底。 示例如下: import copy from 'copy-to-clipboard'; copy('Text'); 相比起 clipboard.js 的使用思路是更加直观了,可惜生不逢时,不如 clipboard.js 出名(也可能有取名的原因在里面)。 VueUse - useClipboard VueUse 实现的这个 useClipboard 是令我最为满意的一个。useClipboard 充分考虑了浏览器的兼容性,在检测到满足 navigator.clipboard 的使用条件时优先使用 navigator.clipboard.writeText() ,在不支持 navigator.clipboard 或者 navigator.clipboard.writeText() 复制失败时转去使用 execCommand 实现的 legacyCopy,并且借助 Vue3 中的 Composables 实现了一个 1.5 秒后自动恢复初始状态的 copied 变量,算是很有心了。 const { text, copy, copied, isSupported } = useClipboard({ source }) </script> <template> <div v-if="isSupported"> <button @click="copy(source)"> <!-- by default, `copied` will be reset in 1.5s --> <span v-if="!copied">Copy</span> <span v-else>Copied!</span> </button> <p>Current copied: <code>{{ text || 'none' }}</code></p> </div> <p v-else> Your browser does not support Clipboard API </p> </template> React 相关生态 React 这边不像 VueUse 一家独大,出现了很多可用的 hooks 库,那就全都过一遍 react-use - useCopyToClipboard react-use 是我能搜到的目前最大的 React Hooks 库,42.9k star。采用的复制方案是直接依赖上面介绍过的 copy-to-clipboard,也就是 execCommand 方案。 const Demo = () => { const [text, setText] = React.useState(''); const [state, copyToClipboard] = useCopyToClipboard(); return ( <div> <input value={text} onChange={e => setText(e.target.value)} /> <button type="button" onClick={() => copyToClipboard(text)}>copy text</button> {state.error ? <p>Unable to copy value: {state.error.message}</p> : state.value && <p>Copied {state.value}</p>} </div> ) } Ant Design - Typography ahooks 是小麦茶第一个报出来的 react hooks 库,由 Ant Design 原班人马维护。不过其在仓库中并没有对剪贴板的封装,因此在小麦茶的建议下我跑去翻了 Ant Design 中的 Typography 对复制能力的实现。和上面的 react-use 一样,都是直接用 copy-to-clipboard,属于 execCommand 方案。 usehooks - useCopyToClipboard 这个库是我问 llm 知道的,现在有 10.5k star。非常逆天的一点在于它的所有逻辑代码都是在 index.js 这样一个单文件里实现的,属实是看不懂了。会先采用 navigator.clipboard.writeText() 尝试写入,失败后再换用 execCommand 的方案。hooks 的用法和上面的 react-use 大差不差。 usehooks-ts - useCopyToClipboard 不知道是不是为了解决上面那玩意儿不支持 ts 才开的库。只使用 navigator.clipboard.writeText() 尝试写入剪切板,失败后直接 console.warn 报错,没有 fallback 方案。 结语 从结果上来看,VueUse 的封装无疑是最令我满意的。优先尝试性能最好的 Clipboard API,再尝试 execCommand 作为回落,同时辅以多个响应式变量帮助开发,但又不擅作主张地使用 prompt 作为保底,最大程度地把操作空间留给开发者。 站在 2025 年的节点回望,前端剪切板操作技术的演进轨迹清晰可见:从早期依赖 Flash 的脆弱方案,到 execCommand 的曲线救国,最终迈向标准化 Clipboard API 的优雅实现。这段历程不仅是技术迭代的缩影,更折射出前端开发中独特的「妥协艺术」。 在未来的很长一段时间里,或许我们还是会在「优雅实现」与「向下兼容」之间寻找平衡点、在浏览器沙箱里戴着镣铐跳芭蕾,但那些为兼容性而生的临时方案,终将成为见证前端进化史的珍贵注脚。 参见 JS复制文字到剪贴板的坑及完整方案。 ZeroClipboard 学习笔记 | 囧克斯 Cut and copy commands | Blog | Chrome for Developers execCommand method (Internet Explorer) | Microsoft Learn sudodoki/copy-to-clipboard zenorocha/clipboard.js useClipboard | VueUse Side-effects / useCopyToClipboard - Docs ⋅ Storybook document.execCommand - Web API | MDN Clipboard.writeText() - Web API | MDN Onclick Select All and Copy to Clipboard? - JavaScript - SitePoint Forums | Web Development & Design Community How would I implement 'copy url to clipboard' from a link or button using javascript or dojo without flash - Stack Overflow

2025/4/21
articleCard.readMore

ssh 拯救世界——通过 ssh 隧道在内网服务器执行 APT 更新

事情的起因是因为精弘的前技术总监抱怨学校的内网服务器无法连接外网,从而导致 apt 安装与更新异常困难,需要手动从源中下载软件包、软件包的依赖及其依赖的依赖。。。然后将这些包通过 sftp/rsync 一类的手段传到服务器上手动安装。 于是本文应运而生,我们可以在本机使用 Caddy (Nginx 当然也行)反代一个 APT 源镜像站,通过 ssh 隧道建立端口转发,这样就可以在内网服务器上访问到本地的 Caddy 服务器,进而访问到外网的镜像站。 前提条件 主控机(你自己的电脑)能够通过 ssh 直接连接电脑(可以是使用一些网络工具),而不是先通过 ssh 登陆到一台中转机,再从中转机登陆到目标服务器。后面这种情况当然也可以使用类似的手段实现我们的目标,但会更复杂一些。 主控机(你自己的电脑)在连接内网服务器的同时,能够连接公网镜像站(不行的话要不然你提前本地同步一份镜像做离线镜像站)。 反代镜像站 我这里选择了 Caddy 而非 Nginx,一方面是 Caddy 的配置文件写起来简单,另一方面 Caddy 是 Golang 编写,一个二进制走天下,Windows 也能直接下载运行。 我们以最常见的清华 tuna 镜像站为例,一个简单的 caddy 配置文件是这样的 :8080 { reverse_proxy https://mirrors.tuna.tsinghua.edu.cn { header_up Host {http.reverse_proxy.upstream.hostport} } } 将上面这段代码保存为 Caddyfile 文件名,随后使用 caddy 命令在保存路径运行 caddy run --config ./Caddyfile 如果没有报错,那你应该能在本地的 8080 端口看到清华的镜像站 你可能注意到,反代后的页面和清华的镜像站有些许差异,没有清华的 logo,这大概是因为页面的 js 对 host 进行了判断,如果不是清华或者北外的页面,就不会添加学校的名称,但这不影响我们从这些镜像站获取更新。 建立 ssh 隧道 建立隧道时,需要使用如下的命令 ssh -R 8085:localhost:8080 root@remote.example.com -R 表示建立反向隧道,其他的参数选项可以参考这一篇博客「SSH 隧道技术」,也是精弘的学长写的。 此时,我们建立了一个内网服务器 8085 端口到本机 8080 端口的 ssh 端口转发。(使用 8085 端口是我为了区分其和 8080 端口,实际上可以使用任何空余端口) 我们可以在服务器上使用 curl 来测试一下是否能够正常访问,我这里简单访问了下 Debian 源根目录下的一个 README 文件。 curl http://localhost:8085/debian/README 换源 所以现在我们在内网服务器的 8085 端口上有一个清华开源镜像站的反代,我们可以通过 8085 端口访问镜像站中的所有内容。 先遵循清华开源镜像站的指示,进行换源,记得一定要勾选「强制安全更新使用镜像」。 随后,我们将源中的所有 https://mirrors.tuna.tsinghua.edu.cn 替换成 http://localhost:8085 sed -i 's|https\?://mirrors\.tuna\.tsinghua\.edu\.cn|http://localhost:8085|g' `grep -rlE 'http(s)?://mirrors\.tuna\.tsinghua\.edu\.cn' /etc/apt/` 可以看到,我们通过 ssh 隧道实现了在内网服务器执行 APT 更新及安装软件。 温馨提示,ssh 隧道在本世纪 10 年代初经常被用来进行搭建一些跨境访问,但因为其独特的流量特征很快淡出了历史舞台,因此不要使用 ssh 进行大量的跨境网络传输,容易被封禁。 当然,实现这一目标的方法是很多的,其他一些例如 frp 的工具同样能做到这种效果,只不过 ssh 隧道这种方案随开随用,随关随停,不需要更多的配置,因此我主要推荐。

2025/3/30
articleCard.readMore

Cudy TR3000 吃鹅(daed)记

缘起 前不久在京东自营看到我馋了很久的 Cudy TR3000 有 ¥153 的折扣价,虽然比起 ¥130 的史低价(甚至 ¥110 的凑单史低价)还有些距离,但已经到我的可接受范围内了,于是果断下单剁手了这台我心心念念的 Cudy TR3000 迷你路由器,以此来缓解我的开学前综合症(一种精神性疾病) 这台路由器使用 Type-C 供电,拥有一个 2.5Gbps 的 WAN 口和一个 1Gbps 的 LAN 口,在此基础上还有一个 USB 口可用于打印机共享、挂载外接存储、安卓手机 USB 共享网络等多种用途。更让我心动的地方在于其小巧的体型,非常适合出差、旅行、短期租房等场景。考虑到接下来一段实习可能会有租房需求,于是便趁此机会果断下单了。 官方系统是基于 openwrt 定制的,功能比较单一,因此考虑刷入 openwrt 原版系统增加可玩性。在恩山无限论坛上发现已经有人编译了基于 Linux 6.6 版本的 OpenWRT 系统,这已经满足了 dae 的 Bind to LAN 功能的内核版本要求( >= 5.17 ),且 512MB 的内存大小刚好达到了推荐的最小内存大小,于是这 dae 肯定是要试着吃一吃的。如果成功了,这就是我手上第一台吃上大鹅的硬路由。 开始刷机 路由器官方系统的后台管理地址是 192.168.10.1,初次进入会要求你设置密码,然后就是一路随便点,完成初始化,随后就进入到主页。我手上这台的 FW 版本号是 2.3.2-20241226,不清楚后续的版本能不能仍然使用这套方案。 过渡固件 首先我们需要先刷入所谓的「过渡固件」。刷入过渡固件的意义在于,这个过渡固件能被官方系统的升级程序所承认,这样就允许我们进行后续的操作。 过渡固件的文件名和 md5 值如下: b8333d8eebd067fcb43bec855ac22364 cudy_tr3000-v1-sysupgrade.bin 随后我们可以在路由器的管理页面的基本设置中找到固件升级的地方,在本地更新一栏中选择过渡固件上传更新即可。 刷入解锁 FIP 分区写入权限的固件 刷入过渡固件后稍等大约一分钟,路由器的 DHCP 重新工作,我们就可以通过 192.168.1.1 进入过渡固件的管理页面。 初次登陆时没有密码,随便输就能登陆成功。考虑到后续可能会有恢复出厂的需求,建议在这一步对 FIP 分区进行备份。 这次我们需要刷入下面这个 LEDE 固件来解锁 FIP 分区的写入权限,文件名和 md5 仍然放在下面 4af5129368cbf0d556061f682b1614f2 openwrt-mediatek-filogic-cudy_tr3000-v1-squashfs-sysupgrade.bin 在下方选择刷入固件,上传我们本次需要刷入的固件,刷入。 刷入 uboot 再等待一分钟左右,电脑重新连接上路由器后,我们可以进入到这个解锁了 FIP 分区写入权限的固件,默认密码是 password 在侧栏选择文件传输,将本次要刷入的 uboot 上传,文件名和 md5 还是放在下面。注意 zip 包要解压 e5ff31bac07108b6ac6cd63189b4d113 dhcp-mt7981_cudy_tr3000-fip-fixed-parts-multi-layout.bin 随后侧栏进入 TTYD 终端,输入默认的用户名密码 root / password,执行命令刷入 uboot mtd write /tmp/upload/dhcp-mt7981_cudy_tr3000-fip-fixed-parts-multi-layout.bin FIP 刷入自编译的 immortalwrt 刷入 uboot 以后,给路由器断电,确保网线分别连接电脑和路由器 LAN 口后,按住 reset 键再插入电源键,直至白灯闪烁四次后转为红灯后松开 reset 键,即可进入 uboot。 我编译的是 112m 的布局,因此需要选择 mod-112m 这个 mtd 布局后上传固件刷入。 8c9a44f29c8c5a0617e61d49bf8ad45d 112m-immortalwrt-cudy_tr3000-ebpf_by_zhullyb_20250325-squashfs-sysupgrade.bin 再次等待电脑重新连接路由器,这是最终吃上 daed 的系统了,依然是没有默认密码,随便输入即可进入。在连接上网络后,在系统 - 软件包页面,更新软件包列表。 随后就可以安装 dae / daed 相关软件了,可视需求选择 luci-i18n-dae-zh-cn 或者 luci-i18n-daed-zh-cn,其他包会作为依赖一同被安装。我这里安装的是 daed。 安装后刷新界面,我们就可以在顶栏的服务板块看到 daed。 daed 正常运行,能正常跑满我家的 300Mbps 宽带下行(单线程实测 250Mbps),速度峰值时 CPU 占用图如下。 文章中提到的文件 https://www.123684.com/s/gfprVv-wEQ8d https://www.123912.com/s/gfprVv-wEQ8d 参见 Cudy TR3000 刷机教程指北 使用 ImmortalWrt+Dae 为 Windows 配置透明代理 cudy tr3000 v1中文三分区DHCP uboot第二版 使用 hanwckf/bl-mt798x 引导主线 OpenWrt 固件 QiuSimons/luci-app-daed

2025/2/28
articleCard.readMore

使用 Cloudflare Workers 监控 Fedora Copr 构建状态

确信,是 cloudflare workers 用上瘾了 在「使用 Github Action 更新用于 rpm 打包的 spec 文件」一文中,我利用 Github Action 实现了自动化的 spec 版本号更新,配合 Fedora Copr 的 webhook 就可以实现 Copr 软件包更新的自动化构建。看似很完美,但缺少了一个构建状态的监控机制,这导致出现构建错误的时候我不能及时得到通知(无论构建错误是 spec 本身的问题或者是构建时的网络环境问题)。 西木野羰基 提出 notifications.fedoraproject.org 可以配置通知,Filters 的 Applications 选项中有 copr,但很可惜,实测没有效果。这里的通知配置的似乎只是邮件的过滤规则——如果 copr 本来就没打算构建失败的时候给你发邮件,那即使建立了过滤规则,依然是不可能收到邮件的。 不过好在 Fedora Copr 本身有非常完备的 api 文档,/monitor 这个 API 能用来获取软件包最新的构建情况。 因此,我们就可以通过 Cloudflare 的 cronjob 定时请求这个接口,查询是否有软件包构建失败。 先来编写打请求的部分 async function fetchCopr() { const ownername = "zhullyb"; const projectname = "v2rayA"; const url = new URL("https://copr.fedorainfracloud.org/api_3/monitor") url.searchParams.set("ownername", ownername) url.searchParams.set("projectname", projectname) const response = await fetch(url) const data = await response.json() if (data.output !== "ok") { throw new Error("Failed to fetch COPR data") } return data } 随后编写通知部分,我这里采用的是飞书的 webhook 机器人 async function notify(text) { const webhook = "https://open.feishu.cn/open-apis/bot/v2/hook/ffffffff-ffff-ffff-ffff-ffffffffffff" const body = { msg_type: "text", content: { text: text } } const response = await fetch(webhook, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }) console.log(response) } 最后就是 cronjob 的调用部分和构建状态解析部分 export default { async fetch(request, env, ctx) { return new Response('Hello World!'); }, async scheduled(event, env, ctx) { const data = await fetchCopr() const errorPackages = new Array() for (const pkg of data.packages) { for (const chroot of Object.values(pkg.chroots)) { if (chroot.state == "failed") { errorPackages.push(pkg.name) break } } } if (errorPackages.length > 0) { await notify(`COPR 以下包发生构建失败:\n${errorPackages.join("\n")}`) } else { console.log("COPR 所有包构建成功") } } }; 随后在 Cloudflare Workers 的 Settings 部分设置好 Cron 表达式即可,我这里选择在每小时的 55 分进行一次检测,这样下来一天只会消耗 24 次 workers 次数,简直毫无压力。 缺点: 我懒得使用持久化数据库记录软件包构建的成功状态,这会导致出现一个包构建失败后,每隔 1 小时都会有一条提醒,什么夺命连环 call。我目前不想修复这个问题,要不然还是降低 cron 的触发频率好了。

2025/2/23
articleCard.readMore

基于 Cloudflare Workers 实现的在线服务状态检测告警系统

起因 受一些客观因素的影响,微精弘前一阵子针对学校教务系统的数据爬取服务状态出现了非常不稳定的状态,而后端在设计初并没有考虑到异常告警机制,恰逢现任员工都身陷期末周的痛苦之中,我这种计院 Lite 专业的精弘老人就打算实现一个针对「微精弘主后端 <-> funnel 爬虫服务 <-> 教务系统」这一条链路的告警机制。旨在短期内(即期末周结束之前)填补微精弘的后端服务告警机制的空白,让运维人员能够及时收到并处理排查后端网络链路的异常情况,尽最大努力保证服务在线率,保障工大本科生在期末周内使用体验。 需求分析 如果你不知道微精弘的具体架构实现,这里有一篇由前技术总监提笔并由现任技术总监完善的架构杂谈「微精弘 | 架构杂谈」,原文最初发表于前者的博客。 基于上述客观条件以及我个人在服务监控告警领域近乎为 0 的经验,我一拍脑袋提出了以下几点需求: 稳定性。告警服务本身必须要比我们的主后端更加稳定,这是告警服务的基础。 针对现有服务的侵入性低。告警服务不能影响到现有服务,最好能够完全分离开来,不应当部署在同一台服务器上。 开发快速。整个服务需要尽快落地,因为现有的服务在一周内出现了三次故障,且由于主动的监控告警机制的缺失,我们每次都要等服务 down 机的两小时后才意识到服务挂掉了,如果真在考试周这个使用高峰期内再出现这样的故障不容允许的(用户需要查询考场信息)。 尽可能低的运维成本。没什么好解释的,谁也不希望一个告警服务占用太多的运维成本,无论是人力上的还是资源上的。 技术选型 结合我已有的经验,我选用了 Cloudflare Worker 来完成这个任务。Cloudflare Workers 本身是支持 cron job 的,能够以分钟级为单位的间隔对服务进行主动探测。Cloudflare 每天都有 10w 次免费的 Workers 调用额度,本身的服务在线率也过硬,唯一的缺点可能就是海外节点访问国内服务器的延迟过高了。不过考虑到我们探测的是在线情况而非延迟情况,倒也不是不能接受。 由于服务特性的关系,我们容许一定的访问失败概率。比如五次访问中如果有两三次失败,我们也认为线路是通的,可能只是教务系统的土豆快熟了。因此并不是每一次失败的探测都需要进行告警。另外,我们还需要记录当前的服务状态,一旦服务被认定为下线状态,后续探测失败我们就不再进行告警,直到我们重新判定服务状态为上线(即每段连续的下线时间都只触发一次告警)。Cloudflare Worker 是一种 serverless 服务,且每次执行探测任务的可能都不是同一个 Cloudflare 的边缘节点,因此我们没法使用变量在内存中记录目前的服务状态,需要引入外部数据库来完成短期内的数据存储。 在数据库上,我们必须选择 Cloudflare 自家的在线数据库服务,通过 Cloudflare 自己内部的网络传输数据库查询结果才能得到尽可能低的延迟。在一番考量过后,KV 数据库和 SQL 数据库中,我果断选择了 Cloudflare D1 这个 SQL 数据库(本质是 SQLite),D1 数据库以更长的查询时间换取数据的实时性。Cloudflare 为免费用户提供了每天 500w 行读取和 10w 行写入的免费额度,只要好好加以利用,就不太可能超出限额。后续我还考虑通过这些数据库的数据使用 Cloudflare Worker 构建 uptime status 的后端 API,实现一个类似 status.openai.com 的在线服务状态可视化界面。 登陆 wrangler wrangler login 项目初始化 由于之前有过一些编写 Cloudflare Workers 的经验,我深知在 Cloudflare 网页的 code server 编辑器上编辑单文件的 worker.js 的不便,选择使用 Cloudflare 官方推出的工具 wrangler 进行项目的初始化。 npm create cloudflare@latest wjh-monitor Q: What would you like to start with? A: Hello World example Q: Which template would you like to use? A: Hello World Worker Q: Which language do you want to use? A: TypeScript 我们只需要在 index.ts 中编写我们的主要逻辑即可。 数据库初始化 wrangler d1 create wjh-monitor-db 随后会输出这些内容,我们需要把这些配置文件写入项目的 wrangler.toml 文件中 [[d1_databases]] binding = "DB" database_name = "wjh-monitor-db" database_id = "ffffffff-ffff-ffff-ffff-ffffffffffff" 编写一个 sql 文件创建数据表,并通过 wrangler 创建它 // schema.sql CREATE TABLE IF NOT EXISTS DATA ( id INTEGER PRIMARY KEY AUTOINCREMENT, check_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, check_item VARCHAR(10), response_time INTEGER NOT NULL, success BOOLEAN NOT NULL, online_status BOOLEAN NOT NULL, notify BOOLEAN DEFAULT FALSE, other TEXT, INDEX check_time_idx (check_time) ); INSERT INTO DATA ( response_time, success, online_status, notify, other ) VALUES (100, 1, 1, 0, 'initial'); 使用 sql 文件创建远程数据表 wrangler d1 execute wjh-monitor-db --remote --file=./schema.sql 随后我们可以使用 wrangler 在远程数据库上执行 query 命令,并获得相应的结果 wrangler d1 execute wjh-monitor-db --remote --command='SELECT * FROM DATA' 主体逻辑编写 说实话,代码部分没什么太多好说的,正要了解思路直接去看源码就好。我在这里简单提两点。 目录下的 worker-configuration.d.ts 定义了 env 变量的类型定义,如果我们在项目中绑定了一些变量(比如我们在 wrangler.toml 中绑定了 d1 数据库,变量名为 DB),需要在这里声明以防止后续 TypeScript 的报错。 在 export default {} 中是将会被导出的主要函数,Hello World 项目编写了 fetch 函数,这是在 workers 被通过 http 方式访问时所调用的,如果要使用 cron job 功能,我们需要编写 scheduled 函数来被 workers 调用,并在 wrangler.toml 中配置好 crontab 触发器。 我们在 wrangler.toml 中绑定了 DB 变量作为数据库的快捷访问方式,因此我们可以在代码中通过 D1 数据库的 Workers Binding API 来实现针对数据库的快捷操作,如 const res = await env.DB.prepare("SELECT * FROM DATA ORDER BY check_time DESC LIMIT 4").run(); 需要注意的是,workers 不存在上下文,每一次访问的处理都是前后独立的,如果你需要临时存储一些数据,不要使用变量,一定要存入持久化存储的数据库。 预览: wrangler dev 部署: wrangler deploy 小插曲 由于我对后端经验的缺乏,我在编写 sql 语句时没有意识到建立索引的重要性。我在查询时使用了 ORDER BY <未建立索引字段> LIMIT 5 的方式来查询最近五次的记录,这导致数据库不得不在我每次查询时都完整遍历一遍整张表。随着 cronjob 每分钟运行时插入一条新数据,记录的行数随时间增加,每次查询的成本也逐渐增加,最终造成了单日访问八百多万行的记录,超出了 Cloudflare 的免费额度,一度造成了项目被迫下线的风险。 所幸 Cloudflare 没有给我停机,而我也及时定位到了问题并补建了索引,使每日的读取量回到了正常的状态。 参见 Cloudflare D1 · Cloudflare D1 docs Cron Triggers · Cloudflare Workers docs Cloudflare D1 使用记录

2025/1/18
articleCard.readMore

构建部署在 Cloudflare Workers 上的 TG Bot

起因 早在去年 10 月,我就写过一篇《创建 b23.tv 追踪参数移除 bot》。记录了部署 b23.tv 的追踪参数移除 Bot 的部署方案。其中提到的 TG Bot 随着服务器到期不再续费也一同落灰了——公益服务总是这样,开始时满腔热血,随着时间散去没有多少人能坚持投入成本,徒留下一地鸡毛。 大概半个月前,我在群里看见 Asuka Minato 开发的群消息总结 Bot,整体部署在 Cloudflare Workers 上,在保证零运营成本的情况下有着相当非常不错的在线率保证,因此便考虑将这个 Bot 迁移到 Cloudflare Workers 上。选用免费的 serverless 能够有效延长服务的可持续性,它不需要额外投入时间精力和财力进行维护,通常可以活很久。 之所以可以把 TG Bot 部署到 Cloudflare Workers 上,主要是得益于 TG 平台支持 webhook 的方式让 Bot 程序提供服务。我们所需要提供的是一个公网能访问的 http 地址,在成功注册 webhook 服务后,TG 的官方服务器会将所有 Bot 所在群或者和所有对个人的对话以 http 请求的方式打到这个 url 上,而 Cloudflare Workers 平台就能提供一个 workers.dev 结尾的公网 url 作为 webhook 提供给 TG 服务器。 怎么做? 在这个 Github 仓库中,给出了不少例子,用以讲述如何利用 Cloudflare Workers 构建 TG Bot。 在 bot.js 这个文件中编写了一个最简单的 TG Bot,其功能是: 将受到的所有消息在头部添加「Echo:」字符串,并发送回刚才的对话。 例: 用户发送 「Hello」,bot 回复 「Echo: Hello」 代码分析 代码整体分为四个部分 顶部变量 文件顶部定义了 Bot 的 token、webhook 的 url 路径以及一个简易的 webhook 密码。 TOKEN 是从 botfather 那里获得的 bot token,SECRET 是自己设置的 webhook 密码,TG 的 webhook 服务器会通过把这个字段添加到名为 X-Telegram-Bot-Api-Secret-Token 的请求头中来证明自己的官方身份。 const TOKEN = ENV_BOT_TOKEN // Get it from @BotFather https://core.telegram.org/bots#6-botfather const WEBHOOK = '/endpoint' const SECRET = ENV_BOT_SECRET // A-Z, a-z, 0-9, _ and - 简易路由 紧接着定义了一个简易的路由 addEventListener('fetch', event => { const url = new URL(event.request.url) if (url.pathname === WEBHOOK) { event.respondWith(handleWebhook(event)) } else if (url.pathname === '/registerWebhook') { event.respondWith(registerWebhook(event, url, WEBHOOK, SECRET)) } else if (url.pathname === '/unRegisterWebhook') { event.respondWith(unRegisterWebhook(event)) } else { event.respondWith(new Response('No handler for this request')) } }) 核心功能 随后的三个函数,会先进行 SECRET 的校验,并将 message 类型消息从 TG 的所有消息类型中分离开单独处理 /** * Handle requests to WEBHOOK * https://core.telegram.org/bots/api#update */ async function handleWebhook (event) { // Check secret if (event.request.headers.get('X-Telegram-Bot-Api-Secret-Token') !== SECRET) { return new Response('Unauthorized', { status: 403 }) } // Read request body synchronously const update = await event.request.json() // Deal with response asynchronously event.waitUntil(onUpdate(update)) return new Response('Ok') } /** * Handle incoming Update * https://core.telegram.org/bots/api#update */ async function onUpdate (update) { if ('message' in update) { await onMessage(update.message) } } /** * Handle incoming Message * https://core.telegram.org/bots/api#message */ function onMessage (message) { return sendPlainText(message.chat.id, 'Echo:\n' + message.text) } /** * Send plain text message * https://core.telegram.org/bots/api#sendmessage */ async function sendPlainText (chatId, text) { return (await fetch(apiUrl('sendMessage', { chat_id: chatId, text }))).json() 我们需要更改的就是这个 onMessage 函数,用户输入的文本信息通过 message.text 获取,我们可以很轻易的把 b23 remover 的逻辑用 js 实现 async function onMessage(message) { if (!message.text) { return; } const b23Reg = /b23\.tv\/[a-zA-Z0-9]+/g; if (!b23Reg.test(message.text)) { console.log('No match found'); } const b23Links = message.text.match(b23Reg); const cleanLinks = []; await Promise.all( b23Links.map(link => b23Remover('https://' + link)) ).then(result => { cleanLinks.push(...result); }); const text2Send = "Track ID removed:\n" + cleanLinks.join("\n"); return sendPlainText(message.chat.id, text2Send); } async function b23Remover (url) { const r = await fetch(url) const v = r.url const u = new URL(v) return u.origin + u.pathname } webhook 注册相关 后面的 registerWebhook、unRegisterWebhook、apiUrl 在没有特定需求的情况下不需要变动。 在 Cloudflare 部署后,就可以访问 aaa.bbb.workers.dev/registerWebhook 这个 url 注册 TG 的 webhook 服务。收到 Ok 就代表注册好了。 成果展示

2024/12/30
articleCard.readMore

2024年,Firefox 是唯一还在坚持执行在线的 SSL 证书吊销状态检查的主流浏览器

小试一下? 在开始阅读后面的内容之前,或许你可以试试看你正在使用的浏览器能不能访问下面两个链接: https://digicert-tls-ecc-p384-root-g5-revoked.chain-demos.digicert.com/ https://revoked-isrgrootx1.letsencrypt.org/ 这两个链接分别是由 digicert 和 Let's Encrypt 维护的,特意维持了一个证书没过期,但被 CA 吊销的状态。 在我的 Firefox for Linux 上,两个链接我都无法打开。 这是预期行为。Firefox 在与目标站点建立 https 链接之前,先向 CA 提供的 OCSP 服务器打了一个 http 请求来判断目标站点的 ssl 证书是否有效(是否没有被 CA 主动吊销)。 在 Firefox for Android 上,我无法打开第一个链接,但可以打开第二个,这是因为 Mozilla 给 Android 用户设置的策略为「只对 EV 证书进行 ocsp 校验,跳过 DV 证书和 OV 证书」,似乎在竞争更加激烈的移动端,Firefox 为了加载速度做出了安全性上的妥协。 而在 Google Chrome / Microsoft Edge 上,OCSP 是不被支持的,chromium 团队在 2014 年就禁用了 OCSP 校验,且目前没有设置项允许用户手动开启,目前它只支持本地的 CRLSets 规则集。 Safari 经我本人测试默认只对 EV 证书进行有效性检验。 SSL 证书被吊销是怎么回事? 在某些情况下(比如用户告知 CA 颁发给自己的证书私钥不慎被泄漏了,或者 CA 的颁发程序出现漏洞被人滥用,需要吊销在此期间发出去的所有证书),CA 需要吊销一些证书,并通过自己的渠道将被吊销的证书尽快告知所有用户——需要被吊销的证书和正常的证书在外观上没有任何区别,用户的浏览器必须依赖 CA 的外部消息通知才能知道哪些证书是被吊销的。 Firefox 是如何接收来自 CA 的吊销信息的? Firefox 通过提供了两种方案,以便用户从 CA 处得知目标站点的证书是否被吊销。 OCSP 正如我前文所说的,Firefox 的 PC 端在与目标站点建立 https 连接之前,会先通过 http 协议向 CA 的 OCSP 服务器打一个 POST 请求来确认证书是否被吊销。 我们可以通过 openssl 拿到目标站点的 ssl 公钥,查看 CA 指定的 OCSP Server openssl s_client -connect example.com:443 -servername example.com | openssl x509 -text -noout 软失败 这个请求一共有三种结果: 请求结果正常,证书没有被吊销:Firefox 继续和目标站点建立 https 连接 请求结果正常,证书被吊销:Firefox 拒绝和目标站点建立 https 连接,如同我在博客开头贴的图 请求结果异常(请求超时):Firefox 继续和目标站点建立 https 连接 透过在第三种情况,我们可以发现,Firefox 对 OCSP 检查的结果是软失败 (soft fail) 态度——即如果在 OCSP 过程中发生异常,Firefox 将不得不放弃 OCSP 检查并放行。根据 Mozilla Blog 的说法,如今有 9.9% 的 OCSP 检查都是超时的。 Firefox 中的相关配置项 在 Firefox 的 about:config 配置中搜索,我们可以看到一些关于 OCSP 的配置项 security.OCSP.enabled: 是否开启 OCSP 检查,默认为 1 0: 关闭 OCSP 检查 1: 开启 OCSP 检查 2: 只对 EV 证书进行 OCSP 检查 security.OCSP.required: 是否一定要通过 OCSP 才允许建立连接(即是否不允许“软失败”),默认为 false false: 允许软失败 true: 不允许软失败 security.OCSP.timeoutMilliseconds.hard: 针对 EV 证书,OCSP 检查的超时时间,默认为 10000 (10 秒) security.OCSP.timeoutMilliseconds.soft: 针对 DV 和 OV 证书,OCSP 检查的超时时间,默认为 2000(2 秒),移动端为 1 秒。 EV 证书相比 DV 和 OV 有更严苛的申请条件,区别可以参考DV、OV和EV SSL证书之间有什么区别? 弊端 ocsp 不能在不增加用户负担的情况下使用硬失败(hard fail),对于无响应或者超时的 ocsp 验证只能直接放行,这意味着攻击者可以直接屏蔽浏览器和 CA 之间的连接,拖到 ocsp 超时,并不能有效保障用户免受攻击。 在 ocsp server 响应或者连接超时前,与目标站点的 https 连接会被阻塞,这带来了更大的访问延迟。有时还会出现用户与 ocsp server 无法连接的情况。 ocsp 是由用户浏览器主动向 CA 发起 http 请求,可能会导致用户隐私泄漏(IP 地址、位置、用户的部分浏览历史记录等)。即使 CA 故意不保留这些信息,地区法律也可能强制 CA 收集这些信息。 OCSP 装订 (OCSP stapling) 这是一种新的方式,由目标站点的服务器主动向 CA 的 ocsp 服务器缓存 ocsp 信息(有效期最长为 7 天),并将其在用户访问时将相关信息一起提供给用户,避免用户直接向 CA 的服务器发起查询请求,能够规避部分弊端(避免 CA 收集用户信息,规避用户与 CA OCSP 服务器连接性不好等问题)。 目前,caddy 是默认开启 OCSP 装订的,而 nginx 可以在配置后手动开启。 可以采用 openssl 来查询目标站点是否开启了 OCSP 装订: openssl s_client -connect example.com:443 -status 如果开启了 OCSP 装订,那么返回的数据中会有 OCSP Response Data 相关的描述 CRLite (WIP) CRLite 是 2017 年 IEEE 安全和隐私研讨会上一组研究人员提出的一项技术,该技术可以有效地压缩吊销信息,使 300 兆字节的吊销数据可以变成 1 兆字节。CRLite 数据由 Mozilla 收集处理后推送给浏览器实现证书状态的本地查找,这项技术仍然处于开发阶段。 当浏览器使用 CRLite 查询对应站点的 ssl 证书的有效状态时,一般会有 5 种查询结果 Certificate Valid,表示 CRLite 权威返回证书有效。 Certificate Revoked,表示 CRLite 权威返回证书已被吊销。 Issuer Not Enrolled, 这意味着正在评估的证书未包含在 CRLite 筛选器集中,可能是因为颁发证书的证书颁发机构 (CA) 未发布 CRL。 Certificate Too New,表示正在评估的证书比 CRLite 筛选器新。 Filter Not Available,这意味着 CRLite 过滤器要么尚未从 Remote Settings 下载,要么已经过时以至于停止服务。 Mozilla 计划每天发布 4 次 CRLite 的更新,以减少第 4 种情况。 速度优势 CRLite 的本地数据查询相比起 OCSP 的在线查询拥有天然的优势,99% 的情况下,CRLite 会比 OCSP 快,其中 56% 的情况下,CRLite 会比 OCSP 快 100 毫秒以上。 此外,当 CRLite 不可用时,Firefox 仍然可以回退到传统的 OCSP 检测机制。 其他 Let's Encrypt 在 2024 年 7 月发布博客,CA/Browser Forum在上一年 8 月通过了一向投票允许 Let's Encrypt 等公开信任的 CA 将设立 OCSP server 作为可选选项。他们计划在未来 6~12 月内宣布关闭 OCSP 服务的时间表。 参见 CA/Revocation Checking in Firefox - MozillaWiki Firefox OCSP policy - Firefox Development - Mozilla Discourse In which browsers is OCSP (Online Certificates Status Protocol) supported in? Introducing CRLite: All of the Web PKI’s revocations, compressed - Mozilla Security Blog CRLite: Speeding Up Secure Browsing - Mozilla Security Blog High-reliability OCSP stapling and why it matters - The Cloudflare Blog Online Certificate Status Protocol - Wikipedia 1429800 - (crlite) [meta] Implement a CRLite based revocation mechanism 1761109 - Make check-revocations mode the default CRLite mode Firefox - The only browser doing certificate revocation checks right : r/browsers Intent to End OCSP Service - Let's Encrypt Revocation checking for EV server certificates in Chrome - Google Groups CRLSets - The Chromium Projects 由于Bug,Let's Encrypt决定吊销300多万张证书! Let’s Encrypt OCSP 域名被封 DV、OV和EV SSL证书之间有什么区别?

2024/11/19
articleCard.readMore

小爱课程表适配不完全指北——以 ZJUT 本科正方教务系统为例

写在前面 一个月前,我发现小爱课程表中针对我学校的教务系统导入系统年久失修,因此我便决定自己另立门户、独立维护一版针对 ZJUT 教务系统课表信息导入的适配项目。 整个流程不难,如果你对于 js 代码和爬虫技术有简单的了解,那么很快就可以上手,我大概只花了 2 小时就完成了 阅读文档-编写代码-自测通过-提交审核 的过程,并在一周内正式上架,得到了身边同学的认可。 在适配过程中,一定要仔细阅读官方文档,所有技术性问题几乎都能通过官方文档解决。这篇博客我尽量详细记录了使用 fetch 打请求获取 json 的正方教务系统适配方案,仅供参考。 官方文档地址: 小爱课程表开发者工具使用教程 我的代码: Github 运行原理 小爱课表获取课表信息的大致流程如下 在你的手机上调用系统 webview 进入你指定的教务系统,让你手动输入账号密码并完成登陆流程 获取含有课表信息的字符串(可以是直接获取页面展示的 html 代码,也可以是利用登陆时获取的 cookie/session 信息直接向后端发送请求拿响应) 解析获取到的字符串,按照小米预先定义好的 json 格式输出 作为适配者,我们需要提供三个代码文件,分别是 Provider、Parser 和 Timer Provider 和 Timer 都在本地登陆好教务系统后的 webview 环境中执行,前者需要返回步骤 2 中描述的带有课表信息的字符串,Timer 则返回课程时间、学期周数等信息。 Provider 获取到的字符串将会成为 Parser 的函数参数,这个函数将会上传到小米的服务器中运行,根据文档所说是为了满足部分开发者保护自己的代码防止被抓包而刻意设计的(虽然我不理解这种东西有什么好闭源的,可能是为了防止友商批量抓包获取解析算法以后直接推出竞品)。 适配实战 安装浏览器插件 首先下载小米提供给我们的资源包,目前我拿到的最新的版本是 v0.3.8,下载链接。注意不要被后缀骗了,这不是个 rar 包,我这里后缀改成 tar 后可以被 ark 正确解压,后缀名是 rar 可能是开发者希望 Windows 用户能用 winrar 进行解压? 这里需要一个 Chromium-based 的浏览器安装小米提供的浏览器插件。 然后使用 Chrome 的「加载已解压的扩展程序」安装整个被解压的目录。Chrome 安装后提示 Manifest version 2 将会在 2024 年被弃用,不知道小米能不能在 Chrome 弃用前支持 Manifest version 3,趁着能用我先不管它。 随后打开自己学校的教务网站,F12 打开开发者工具,可以看到多了一栏叫「AISchedule」的选项 随后正常登陆自己的小米账号,放一旁备用。 抓数据包 随后,登陆自己的教务网站,打开课表页面,查看 F12 开发者工具的网络一览,刷新页面加载自己的课表,查看开发者页面中显示的数据流,找到含有课表信息的那一个。 这个流程我个人用惯了 Firefox 浏览器,因此数据分析这一块的截图都是 Firefox 的截图。 在我的例子中,第一个请求的响应是一个 html 页面,勾勒出了这个页面的大致轮廓,不过没有样式。这是一个好的迹象,说明这大概率是一个前后端分离的站点,授课数据很可能是通过 json 的数据单独传递给前端的,我们就不需要从 html 中解析我们的课表。 如果能确定是前后端分离的站点,我们可以尝试勾选这里的「XHR」选项,XHR 的全名是 XMLHttpRequest,是一种前端向后端发起请求的方式,前后端的数据一般都会在这里展示。 我在第三个请求中发现了我的课表信息,在已经登陆的情况下,小米的课程表允许我通过 fetch 函数打一个相同的请求给后端,获取这个响应结果作为 Provider 部分的输出字符串。 调试 fetch 参数 这个请求的 fetch 函数如何构建?可以直接右键这个请求,在菜单中选择「复制为 Fetch 语句」 复制下来的语句长下面这个样子 await fetch("http://www.gdjw.zjut.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", { "credentials": "include", "headers": { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:133.0) Gecko/20100101 Firefox/133.0", "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", "X-Requested-With": "XMLHttpRequest" }, "referrer": "http://www.gdjw.zjut.edu.cn/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default", "body": "xnm=2024&xqm=3&kzlx=ck&xsdm=", "method": "POST", "mode": "cors" }); 先看 body 部分 xnm=2024 很明显是年份的意思 xqm 表示学期,这个通过多次尝试获取不同学期的课表可以得出结果,3 表示第一学期,12 表示第二学期,16 表示第三学期(短学期) 其余的参数我不关心,一模一样带上就行 这个函数是可以直接放在 F12 开发者工具的控制台中运行的,通过在 fetch 函数的尾部(分号前)添加一个回调函数就可以打印出函数获取的结果。 await fetch(...).then(response => response.json()) 这就允许我们去尝试是否可以删减一些 fetch 函数的参数,获得相同的结果。 如删除 headers 中的 User-Agent、删除 referer 等等,我最后精简了一些参数,fetch 函数长这个样子。 await fetch("http://www.gdjw.zjut.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", { "credentials": "include", "headers": { "Accept": "*/*", "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", "X-Requested-With": "XMLHttpRequest" }, "body": "xnm=2024&xqm=3&kzlx=ck&xsdm=", "method": "POST", "mode": "cors" }); 现在,将 fetch 函数自身和其执行后获取的结果复制下来保存到本地备用,我们就可以开始写代码了。 安装依赖 在刚刚的压缩包解压出来的目录中,有一个叫 localTools 的文件夹,我们可以把它复制到自己的工作目录下,我们的代码工作主要就是在那里进行。 打开 VSCode,命令行运行 pnpm i 安装其运行时的依赖,顺便截图给你们看一眼目录结构。 编写 provider 首先编写 code/provider.js 在这个文件中,我们需要执行 fetch 函数,return 其获取到的 json 数据。 按照官方文档,先 loadTool await loadTool('AIScheduleTools') 随后,我选择使用 AISchedulePrompt 这个小米封装的工具让用户手动输入需要导入课表的学年和学期信息,并对输入数据进行简单校验。 const year = await AISchedulePrompt({ titleText: '学年', tipText: '请输入本学年开始的年份', defaultText: '2024', validator: value => { try { const v = parseInt(value) if (v < 2000 || v > 2100) { return '请输入正确的学年' } return false } catch (error) { return '请输入正确的学年' } } }) const term = await AISchedulePrompt({ titleText: '学期', tipText: '请输入本学期的学期(1,2,3 分别表示上、下、短学期)', defaultText: '1', validator: value => { if (value === '1' || value === '2' || value === '3') { return false } return '请输入正确的学期' } }) const xqm = { '1': '3', '2': '12', '3': '16', }[term] 随后将学年和学期信息拼入 fetch 函数,打出请求,并将返回的 json 数据转为 string 作为函数的返回值 const res = await fetch("http://www.gdjw.zjut.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", { "headers": { "accept": "*/*", "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" }, "referrerPolicy": "strict-origin-when-cross-origin", "body": `xnm=${year}&xqm=${xqm}&kzlx=ck&xsdm=`, "method": "POST", "mode": "cors", "credentials": "include" }) const ret = await res.json() return JSON.stringify(ret.kbList) 最外层使用 try-catch 做简单的异常处理,如果出错就让用户确认是否正确登陆了教务系统 async function scheduleHtmlProvider() { await loadTool('AIScheduleTools') try { //... } catch (error) { await AIScheduleAlert('请确定你已经登陆了教务系统') return 'do not continue' } } 编写 parser 官方文档对 parser 函数做了明确的规范,格式如下 [ { name: "数学", // 课程名称 position: "教学楼1", // 上课地点 teacher: "张三", // 教师名称 weeks: [1, 2, 3, 4], // 周数 day: 3, // 星期 sections: [1, 2, 3], // 节次 },{ name: "数学", position: "教学楼1", teacher: "张三", weeks: [1, 2, 3, 4], day: 1, sections: [1, 2, 3], }, ] 最外层是一个 array,里面包含若干个 object,每个 object 表示一节课 课程名称:String 长度50字节(一汉字两字节) 上课地点:String 长度50字节(一汉字两字节) 教师名称:String 长度50字节(一汉字两字节) 周数:Number[] [1,30] 之间的整数 超出会被裁掉 星期:Number [1,7] 之间的整数 节次:Number [1,30] 之间的整数 (默认[1,12]) 根据后续时间设置自动裁剪 (只有这节课的星期几、上课地点、上课时间都一致才算一节课。比如我一周上两节算法课,周一 34 节和周三 56,那么这应该分成两个 object 来写) 这部分代码没什么好说的,就是对 provider 传过来的 string 用 js 做字符串解析,最后把整个 array 作为返回值返回就行,需要注意处理数组越界、空数组等等的低级错误。 第一步是将刚才的字符串转成 json 格式(如果你采用的是解析 html 的方式,请参考官方文档) function scheduleHtmlParser(json_str) { courses_json = JSON.parse(json_str) const courseInfos = [] //... return courseInfos } 如果需要一个临时的测试方案,可以用以下的测试结构 function scheduleHtmlParser(json_str) { courses_json = JSON.parse(json_str) const courseInfos = [] //... return courseInfos } input_text = ` // 这里是我在上文中复制的 fetch 函数输出的 json 结果 ` console.log(scheduleHtmlParser(input_text)) 执行 node code/parser.js ,观察输出结果是否和官方要求的结构严格一致。 编写 timer timer 函数的运行环境和 provider 一致,都允许你使用 fetch 向教务系统打请求,或者解析页面的 html 函数,但我们学校的教务系统一不写第一周的周一是几号,二不写每节课具体的上下课时间,所以我把我校的相关数据都进行了硬编码,这里直接放个官方文档的示例文件。 /** * 时间配置函数,此为入口函数,不要改动函数名 */ async function scheduleTimer({ providerRes, parserRes } = {}) { // 支持异步操作 推荐await写法 // 这是一个示例函数,用于演示,正常用不到可以删掉 const someAsyncFunc = () => new Promise(resolve => { setTimeout(() => resolve(), 1) }) await someAsyncFunc() // 这个函数中也支持使用 AIScheduleTools 譬如给出多条时间配置让用户选择之类的 // 返回时间配置JSON,所有项都为可选项,如果不进行时间配置,请返回空对象 return { totalWeek: 20, // 总周数:[1, 30]之间的整数 startSemester: '', // 开学时间:时间戳,13位长度字符串,推荐用代码生成 startWithSunday: false, // 是否是周日为起始日,该选项为true时,会开启显示周末选项 showWeekend: false, // 是否显示周末 forenoon: 1, // 上午课程节数:[1, 10]之间的整数 afternoon: 0, // 下午课程节数:[0, 10]之间的整数 night: 0, // 晚间课程节数:[0, 10]之间的整数 sections: [{ section: 1, // 节次:[1, 30]之间的整数 startTime: '08:00', // 开始时间:参照这个标准格式5位长度字符串 endTime: '08:50', // 结束时间:同上 }], // 课程时间表,注意:总长度要和上边配置的节数加和对齐 } // PS: 夏令时什么的还是让用户在夏令时的时候重新导入一遍吧,在这个函数里边适配吧!奥里给!————不愿意透露姓名的嘤某人 } 调试阶段 运行 pnpm main 能调起一个临时的服务器和浏览器插件进行互动,将编辑器的代码实时同步到浏览器插件。 在浏览器插件中,先创建一个新项目 保存后再次打开,选择「编写代码」按钮 检查自己的代码是不是被实时同步到了浏览器插件中,然后可以点击右上角的「本地测试」按钮 如果本机测试出现了问题,可以使用 console.log 语句进行 debug ,问题可能会出现在 F12 开发者工具的控制台,也可能会出现在 vscode 的终端中。确认本机测试无问题后,点击右上角蓝色的「上传」按钮,就可以在上传到云端,在登陆了自己小米账号的手机中找到自己适配的教务导入测试项目,顺利完成导入后给自己的满意度打满分,就可以在浏览器插件中点击上传审核,审核通过后你的适配工作就会公开啦。

2024/11/18
articleCard.readMore

将博客从 waline v2 更新到 waline v3

waline 更新到 V3 版本已经是九个月前的事情了,眼瞅着 hexo fluid 主题并没有带我更新的意思,我就打算自己更新到最新版,结果遇到了两个坑,写文供大家参考。 在 Hexo 目录下的 _config.fluid.yml 文件中找到 waline 的 cdn,将版本号指向最新版。 - waline: https://registry.npmmirror.com/@waline/client/2.15.8/files/dist/ + waline: https://registry.npmmirror.com/@waline/client/^3/files/dist/ 插曲一——waline 不加载 再次部署博客,我遇到了第一个坑:waline 没有在页面上正常加载。 打开控制台一看,报错给得很明白:Waline is not defined 根据 issue#2483, https://unpkg.com/@waline/client@3.1.3/dist/waline.umd.js 用这个地址 本质是 ...... 没有使用 ES Module 的加载方式 <script type="module">,所以需要使用 UMD 的模块 那么按照正常的脑回路,我们应该修改主题中的引入 waline 部分的 js 文件,把针对 waline.js 的引用改成 waline.umd.js,具体的修改处在这里 但是,我是使用 npm 安装的主题,方便在主题更新时直接同步上游的更改,如果使用这种修改源文件引用的方式将会导致我不得不放弃原有的主题安装方式,改用下载主题源码的方式。 那么有没有办法,既能够成功加载带 umd 的 js,又不用改动主题源码呢?还真有,我自己部署 cdn 就好了。从 npmjs 下载带 umd 的 waline.umd.js,重命名成 waline.js,和 waline.css 一起放在同一路径下,在把自己的 cdn 链接前缀填入 _config.fluid.yml 中,就可以实现移花接木——表面上访问的是 waline.js,实际上内容是 waline.umd.js 。 插曲二——重复显示的 @ 更新到 v3 版本以后,我发现所有的评论都出现了重复两遍的 @ 我去群里提问,管理员 li zheming 给出了这样的答复: 这个是 feature 了,早版本 @ 是渲染在评论内容里的,这块后来重构了下,@ 是 Waline 自己渲染了。不过历史数据我们并没有处理,所以会出现这种情况。如果比较介意的话可以手动去数据库里删除下。 那么很显然,我很介意这点,我需要删除数据库中的 @ 信息。 打开 waline 的后台管理站点,我发现我有整整 30 页的评论——很显然我是 waline 的牢用户了,我不太可能一个一个手动去掉评论中的 @ 我的 waline 数据库是 leancloud,对方的 webui 没办法帮助我批量去除 html 或者 markdown 形式的内容(就算对方支持 sql 语句,处理这个问题都够呛),我需要一个脚本来直接处理数据库中的信息。 首先,我们需要导出数据库数据,自然是登陆 leancloud,然后找到 数据存储 - 导入导出 - 数据导出,选择 Comment 单个 Class,单击导出按钮。 err,我寻思凌晨 1 点应该是 16 点前吧,怎么导出不了,而我昨晚 23 点反而可以导出,leancloud 到底是哪门子时区。所以我直接拿了 23 点时导出的数据进行处理。 leancloud 导出的数据是 jsonl 格式的,我们对需要去除的 @ 信息进行归纳总结,发现一共有两种 @ 的渲染方式 <a class="at" href="#id">@username</a> 的 html 风格(有时 class 和 href 顺序还会反过来) [@username](#id) 的 markdown 风格 而 html 风格还有两种结尾方式, 一种是如 <a class="at" href="#id">@username</a> , 这样以 空格 + 半角逗号 + 空格 结尾的形式, 令一种是如 <a class="at" href="#id">@username</a>: 这样以 半角冒号 + 空格结尾的形式。 markdown 风格的结尾方式我就只看到一种,如 [@username](#id): 这样以 半角冒号 + 空格结尾的形式。 因此,我们需要对三种形式分别编写正则表达式进行匹配并删除,参考代码如下 import re with open('Comment.0.jsonl', 'r') as f: s = f.read() patterns = [ r'<a class=\\"at\\" href=\\"#(?:.*?)\\">@(?:.*?)</a>: ', r'<a class=\\"at\\" href=\\"#(?:.*?)\\">@(?:.*?)</a> , ', r'\[@(?:.*?)\]\(#(?:.*?)\):' ] for pattern in patterns: s = re.sub(pattern, "", s) with open('Comment.1.jsonl', 'w') as f: f.write(s) 随后删除 Comment 表中所有数据,把生成的 Comment.1.jsonl 导入 leancloud,就算是大功告成了。

2024/11/15
articleCard.readMore

给家里云装上 Fedora 41 KDE 后,我是如何配置的

前两天给自己的 N100 小主机重装成了最近发布的 Fedora 41 ( KDE ),也是花了不少时间把整个系统调成自己熟悉的样子,因此开一篇博客记录一下。以下仅为我个人的 HomeServer 小主机使用,不具有普适性。 换官方源 我这里比较适合用上交的源,直接参考他们的文档。 sudo sed -e 's/^metalink=/#metalink=/g' -e 's|^#baseurl=http://download.example/pub/|baseurl=https://mirror.sjtu.edu.cn/|g' -i.bak /etc/yum.repos.d/{fedora.repo,fedora-updates.repo} 加 rpmfusion 源 参考 help.cernet.edu.cn 提供的文档 安装源配置文件 sudo yum install --nogpgcheck https://mirror.sjtu.edu.cn/rpmfusion/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm https://mirror.sjtu.edu.cn/rpmfusion/nonfree/fedora/rpmfusion-nonfree-release-$(rpm -E %fedora).noarch.rpm 换源 sudo sed -e 's!^metalink=!#metalink=!g' \ -e 's!^mirrorlist=!#mirrorlist=!g' \ -e 's!^#baseurl=!baseurl=!g' \ -e 's!https\?://download1\.rpmfusion\.org/!https://mirror.sjtu.edu.cn/rpmfusion/!g' \ -i.bak /etc/yum.repos.d/rpmfusion*.repo dnf 操作默认使用 [Y/n] sudo sh -c "echo 'defaultyes=True' >> /etc/dnf/dnf.conf" 移除不想要的软件 libreoffice sudo dnf remove libreoffice* discover, flatpak sudo dnf remove discover flatpak podman sudo dnf remove podman 关闭 selinux sudo sed -i "s|SELINUX=enforcing|SELINUX=disabled|" /etc/selinux/config vlc,mpv,ffmpeg (补全大部分编码器) sudo dnf install vlc mpv ffmpeg --allowerasing docker sudo dnf install docker rustdesk 直接去官方的 Github Release 下载安装包 sudo dnf install https://github.com/rustdesk/rustdesk/releases/download/1.3.2/rustdesk-1.3.2-0.x86_64.rpm 尽管 Rustdesk 支持被控端使用 wayland,但因为权限原因需要被控端手动选择被控区域,不适合无人值守的环境,因此还是要换 x11。 安装 x11 支持 sudo dnf install plasma-workspace-x11 使用 x11 启动 sddm sudo sed -i "s|^#DisplayServer=wayland|DisplayServer=x11|" /etc/sddm.conf 开发相关 sudo dnf install gcc g++ python3-devel 解除 systemd-resolved 53 端口占用 编辑 /usr/lib/systemd/resolved.conf,取消注释,yes 改 no [Resolve] # Some examples of DNS servers which may be used for DNS= and FallbackDNS=: //... #DNS= #FallbackDNS= #Domains= #DNSSEC=no #DNSOverTLS=no #MulticastDNS=yes #LLMNR=yes #Cache=yes #CacheFromLocalhost=no -#DNSStubListener=yes +DNSStubListener=no #DNSStubListenerExtra= #ReadEtcHosts=yes #ResolveUnicastSingleLabel=no #StaleRetentionSec=0 配置 fcitx5 sudo dnf install fcitx5-chinese-addons kcm-fcitx5 fcitx5-autostart 在 设置 - 输入法 中添加拼音 安装词库 https://github.com/felixonmars/fcitx5-pinyin-zhwiki https://github.com/outloudvi/mw2fcitx 在 wayland 下使用 参考 处理 fcitx5 的文字候选框在 tg 客户端上闪烁的问题 if [ ! "$XDG_SESSION_TYPE" = "tty" ] # if this is a gui session (not tty) then # let's use fcitx instead of fcitx5 to make flatpak happy # this may break behavior for users who have installed both # fcitx and fcitx5, let then change the file on their own export INPUT_METHOD=fcitx export GTK_IM_MODULE=fcitx export QT_IM_MODULE=fcitx export XMODIFIERS=@im=fcitx fi +if [ "$XDG_SESSION_TYPE" = "wayland" ] +then + unset QT_IM_MODULE +fi 然后仍然要去 设置 - 键盘 - 虚拟键盘 中选中 fcitx5 安装 vscode sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc echo -e "[code]\nname=Visual Studio Code\nbaseurl=https://packages.microsoft.com/yumrepos/vscode\nenabled=1\ngpgcheck=1\ngpgkey=https://packages.microsoft.com/keys/microsoft.asc" | sudo tee /etc/yum.repos.d/vscode.repo > /dev/null sudo dnf install code 网络优化工具 略 禁用防火墙 sudo systemctl disable firewalld --now RPM 构建 参考 以 Archlinux 中 makepkg 的方式打开 rpmbuild

2024/11/1
articleCard.readMore

为 Hexo 添加 follow 认证

前言 Follow 从今天开始不需要邀请码就可以开始使用部分功能了,除了只能订阅五个订阅源、成就系统没开放、签到不能获得 power 以外,还有部分功能没有解锁(如下图) 我注意到 Follow 的认证机制目前对于 Hexo 用户还是相对不友好的,起码对于 Hexo 用户来说。 「内容」方案要我们在网页(也可能是 rss,follow 没有给出非常明确的指示)上添加非常明显的一段文本,我并不是很喜欢这种行为。 This message is used to verify that this feed (feedId:56144913816835091) belongs to me (userId:70410173045150720). Join me in enjoying the next generation information browser https://follow.is. 「描述」方案要求我们在 rss 的 xml 文件的 <description /> 字段添加一段丑丑的代码。无论是使用 follow 的读者还是其他 rss reader 的读者都会看到博客的 description 中含有一段丑丑的代码,这对于强迫症的我来说是无法忍受的,更别提 atom 类型的订阅根本没有这个字段。 feedId:56144913816835091+userId:70410173045150720 「RSS 标签」方案是我唯一能接受的方案,这个方案需要我们在 rss 的 xml 文件中添加一个名为 <follow_challenge> 的标签,或者是 json 文件中的一个 follow_challenge 对象。虽然具有一定的侵入性,但对于读者来说不会受到影响——应该没有除了 follow 以外的 rss reader 对这个字段进行解析。 <follow_challenge> <feedId>56144913816835091</feedId> <userId>70410173045150720</userId> </follow_challenge> { "follow_challenge": { "feed_id": "56144913816835091", "user_id": "70410173045150720" } } 正篇 那么问题来了,Hexo 用户应该如何使用「RSS 标签」的方案给我们的博客进行 Follow 认证呢? 首先确认前提,我在使用 hexo-generator-feed 这个 npm 库来生成 Hexo 博客的 rss 订阅文件。 在项目的 README 文件中,我们知道可以在 _config.yml 文件中指定 rss 生成时使用的模板文件。模板文件位于 ./node_modules/hexo-generator-feed 路径下,atom.xml 和 rss2.xml 就是这个库所使用的模板文件。我正在使用 atom,所以我把 atom.xml 复制一份放到博客的根目录下魔改模板。下面是 _config.yml 的 feed 配置,你可以看到我在最后两行指定了 template 模板文件。 feed: type: atom path: rss.xml limit: 0 hub: content: true content_limit: content_limit_delim: ' ' template: - atom.xml 如果是个人用途,其实可以直接硬编码,在文件的倒数第二行把我们复制的 <follow_challenge> 放进去。 或者如果我们想要写得考究一些,那么可以是下面这个样子的 feed: template: - atom.xml follow_challenge: feedId: 56144913816835091 userId: 70410173045150720 <!-- //... --> {% endfor %} {% if config.feed.follow_challenge %} <follow_challenge> <feedId>{{ config.feed.follow_challenge.feedId }}</feedId> <userId>{{ config.feed.follow_challenge.userId }}</userId> </follow_challenge> {% endif %} </feed> (说起来,这两个小改动一改,其实完全可以上传 npmjs.com 作为一个新的插件,不过我有点懒了) 文末附一个 follow 邀请码: 6O0oBazB9s

2024/10/23
articleCard.readMore

使用 GPT 对 waline 的评论进行审查

前一阵子收到了这么一条来自 waline 的评论提醒。 New comment on 竹林里有冰的博客 【网站名称】:竹林里有冰的博客 【评论者昵称】:专业数据库 【评论者邮箱】:rakhiranijhhg@gmail.com 【内容】:总之,优化专业数据库对于保持数据准确性、提高系统性能和推动业务成功至关重要。通过遵循本文中概述的策略,您可以提高数据库操作的效率并释放新的增长机会。 【地址】:https://zhul.in/2021/04/04/yay-more/#66f7a8889ab78865d5f5ae19 评论的内容不仅透露着一股 AI 味,还和文章内容可谓是一点关系都没有,点开评论者的网站一看,一股塑料机翻味,怕是又是个来蹭 SEO 的广告哥。 根据 waline 的官方文档所言,waline 是使用了 Akismet 提供的垃圾内容检测服务的。可惜它似乎对 AI 生成的垃圾没有分辨能力。因此我计划使用 GPT 代替 Akismet 对 waline 的新评论进行审核。 walinejs/plugin 提供了一个 tencent-cms 的插件,功能是使用腾讯云的内容审查接口审查评论内容,这和我们需要的功能很像,主体部分和调用方法可以直接借鉴。 // index.js const tencentcloud = require("tencentcloud-sdk-nodejs-tms"); const TmsClient = tencentcloud.tms.v20201229.Client; module.exports = function({secretId, secretKey, region}) { if (!secretId || !secretKey || !region) { return {}; } const clientConfig = { credential: { secretId, secretKey, }, region, profile: { httpProfile: { endpoint: "tms.tencentcloudapi.com", }, }, }; return { hooks: { async preSave(data) { const { userInfo } = this.ctx.state; const isAdmin = userInfo.type === 'administrator'; // ignore admin comment if (isAdmin) { return; } const client = new TmsClient(clientConfig); try { const resp = await client.TextModeration({ Content: data.comment }); if (!resp.Suggestion) { throw new Error('Suggestion is empty. Tencent Cloud TMS info:', resp); } switch(resp.Suggestion) { case 'Pass': data.status = 'approved'; break; case 'Block': data.status = 'spam'; break; case 'Review': default: data.status = 'waiting'; break; } } catch(e) { console.log(e); data.status = 'waiting'; } }, }, }; } 可以看到,我们需要在这个被 module.exports 导出的函数中,return 一个对象,如果使用 hooks 编写的话可以调用一些生命周期 hook: 在 preSave 阶段,我们可以通过标注 data.status 参数来反馈评论类型。approved 为接受,spam 为垃圾邮件,waiting 为等待人工审核;除此之外,还可以基于 Koa 中间件制作插件,文档中有具体的描述。 index.js 顶部是需要引入的依赖。当然,如果需要引入外部的第三方包的话,需要在 packages.json 中加入需要的依赖(使用包管理器的命令进行安装)。 有了这些基础知识,就能手搓一个基于 GPT 的评论审查插件。 OpenAI 提供的是标准的 Restful API,本身的鉴权逻辑也不复杂,其实没必要调用 SDK,直接使用 fetch 调用就行。 const doReview = async (comment) => { const response = await fetch(openaiBaseUrl + '/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${openaiApiKey}`, }, body: JSON.stringify({ model: openaiModel, messages: [ { role: 'system', content: prompt }, { role: 'user', content: comment, }, ], }), }); const data = await response.json(); if (data && data.choices && data.choices.length > 0) { return data.choices[0].message.content; } else { return 'waiting'; } } 再配合相应的封装,一款基于 GPT 的 waline 评论审核插件就完成了 zhullyb/waline-plugin-llm-reviewer 如何安装 npm install waline-plugin-llm-reviewer 如何使用 // index.js const Waline = require('@waline/vercel'); const GPTReviewer = require('waline-plugin-llm-reviewer'); module.exports = Waline({ plugins: [ GptReviewer({ openaiBaseUrl: process.env.OPENAI_BASE_URL, openaiModel: process.env.OPENAI_MODEL, openaiApiKey: process.env.OPENAI_API_KEY, openaiPrompt: process.env.OPENAI_PROMPT, }) ] }); 环境变量 ASISMET_KEY: Waline 使用的反垃圾评论服务,建议设置为 false 以禁用。 OPENAI_BASE_URL: API 基础 URL。例如 https://api.openai.com OPENAI_MODEL: 模型名称。例如 gpt-4o-mini OPENAI_API_KEY: API 密钥。例如 ak-xxxxxx OPENAI_PROMPT(可选): 模型的提示。例如 这是一个评论审查: 在 waline 中设置好对应的环境变量,使用 npm 安装好对应的包,就算大功告成了。

2024/10/12
articleCard.readMore

基于 JavaScript 的 Hexo Fluid 主题 banner 随机背景图实现

为什么要换掉随机图片 API 因为 API 太慢了。根据 PageSpeed 的测速,使用 API 的图片加载时间来到了整整 2.5s,这似乎有些不可忍受。 Vercel 冷启动问题 当初年少无知,为了实现 banner 随机背景图,选择了使用 vercel 创建随机图片 API。这带来了一些问题,首先 vercel 在站点一段时间没人访问以后会进入一种类似休眠的模式,下一次启动将会经历一个冷启动(cold start)的过程。我认为这对于一个图片背景的随机 API 而言是不可忍受的。 观察图上就可以发现,第一次访问时花费了 1.9 秒,第二次只需要 0.5 秒,这是因为第一次是冷启动,需要花费更多时间。 多一次网络请求 抛开冷启动不谈,引入 API 就会导致一次额外的网络请求。访客的浏览器将会先请求随机图片 API,然后根据 API 返回的 302 相应去请求真正的图片,而且这一过程是没法并行的,只能串行执行,这会浪费更多的等待时间。 Vercel 在大陆境内的访问质量 Vercel 在大陆境内的访问质量其实并不算好,即使是使用了所谓的优选节点,也不一定能保证整个大陆境内大部分访客都有不错的访问质量,因此使用 Vercel 搭建 API 的行为并不是最优解。 转向 JavaScript 实现 这个方案本身没多少复杂的,只不过是三年前的我对前端一无所知不敢操刀罢了。 删除原有的背景图 在 _config.fluid.yml 中,将所有的 banner_img: 字段全部置空,防止其加载默认的 /img/default.png 而白白浪费用户的流量。这个字段一共在配置文件中出现了九次。 添加 js 我们的目标是修改 id 为 banner 的 div 块的 backgroud 的 css 属性,Hexo Fluid 默认的生成内容是这样的 <div id="banner" class="banner" parallax=true style="background: url('/img/default.png') no-repeat center center; background-size: cover;"> 我们可以通过 id 来定位这个元素,修改其 style.background 属性。 可以在任何地方引入下面的 js 代码,在这篇名为《Fluid -23- 添加 Umami 统计》 的文章里的方案是可供参考的。 const imgs = [ "https://example.com/1.jpg", "https://example.com/2.jpg", "https://example.com/3.jpg", "https://example.com/4.jpg", "https://example.com/5.jpg", "https://example.com/6.jpg", "https://example.com/7.jpg", "https://example.com/8.jpg", "https://example.com/9.jpg", "https://example.com/10.jpg", "https://example.com/11.jpg", "https://example.com/12.jpg", "https://example.com/13.jpg", "https://example.com/14.jpg", "https://example.com/15.jpg", "https://example.com/16.jpg", "https://example.com/17.jpg", "https://example.com/18.jpg", "https://example.com/19.jpg", "https://example.com/20.jpg", ] const luck_img = imgs[Math.floor(Math.random() * imgs.length)] const banner = document.getElementById('banner') banner.style.background = `url(${luck_img}) center center / cover no-repeat` 成果 博客能够在不引入外部 api 的情况下通过 js 自主实现随机的 banner 背景图,但 pagespeed 的测速结果并没有明显好转,因为 pagespeed 模拟了低速 4G 的访问速度,无论如何都无法提升大文件的加载速度。不过避免了多一次网络请求后,打开页面时的加载速度确实有提升。 参见 How can I improve function cold start performance on Vercel?

2024/9/25
articleCard.readMore

使用向日葵智能插座 C2 用电记录推算宿舍上次烧水时间

我宿舍里入口处有一张公用的桌子,上面有一个烧水壶。根据生活经验,当用手摸烧水壶外壳能感受到明显热量时,水壶内的水大概是两小时内烧的,绝对能喝;但如果用手摸烧水壶外壳感受不到明显热量时,水壶内的水就不知道是什么时候烧的了,可能是三小时前,也可能是三天前。此时,在不寻求外部科学仪器介入的情况下,唯一能做的是询问寝室成员上一次水是谁烧的,是什么时候烧的。但寝室成员并不总是能够及时回答,可能在睡觉,也可能不在寝室里,还有可能出现记忆错乱。 因此,我们需要一种可靠的方案获取上一次烧水时间。 前两天陪黄老板出门吃宵夜的时候和他提到了这个难题,我提出在烧水壶附近加装物理按钮,按动时向局域网内的 HomeServer 发送请求记录准确的烧水时间。他提出可以在烧水壶前加装智能插座,使用智能插座的耗电量来推算上一次烧水时间。这是一个可行方案,上次烧水时间不需要分钟级的精准度,小时级的精准度在这个需求上完全够用,这是一个更好的方案。 在「使用 Root 后的安卓手机获取向日葵智能插座 C2 的开关 api」这篇文章中,我有过抓包向日葵官方 app 的流量数据的经验,这一次直接故技重施。很可惜,我发现用电量数据并不能直接从局域网内向智能插座获取,必须要从向日葵官方的服务器拉下来。其实想想也知道,用电数据一旦精确到小时级,日积月累下来会对硬件的存储提出一定的挑战,而比较合理的方案就是由硬件向官方的服务器每小时通信一次记录下来。 不过好消息是,官方服务器的这个接口并没有进行鉴权,不需要进行额外的操作,一条 curl 命令都能下载下来。 https://sl-api.oray.com/smartplug/powerconsumes/${SN} SN 码也不需要自己去抓包,直接在官方应用的设备关于页面就能看到。 json 数据的结构很明显,最外层是一个 Array,里面有若干个 object [ { "consume": 0, "starttime": 1727125200, "endtime": 1727128740, "index": 0 }, ... ] consume: 这段时间消耗的用电量,单位 Wh starttime: 开始时间,unix 时间戳 endtime: 结束时间,unix 时间戳 index: 智能插座的第几个孔位(为插排预留的参数,智能插座只有 0 这一个位置) 所以我们要做的就是每小时下载一次这个 json 文件,需要时从 json 中寻找上一次用电量较高的小时,取那个小时的 starttime 时间戳转换为东八区人类可读的时间即可。 def last_water(): with open('power.json', 'r') as f: powers = json.load(f) for i in powers: if i.get('consume') >= 30: t = i.get('starttime') break last_water_time = datetime.datetime.fromtimestamp(t) now = datetime.datetime.now() time_delta = now - last_water_time sec = time_delta.total_seconds() hours = sec / 3600 lwt_str = last_water_time.strftime('%m月%d日%H点') return f"上次烧水时间为「{lwt_str}」,距离现在「{hours:.2f}」小时" 至于每小时下载的任务,我这里是使用 crontab + curl 命令实现的,用 python 写个死循环跑也可以。 那么数据都取到了,剩下的就是人机交互的部分,这部分夸张点的可以写 web,写小程序,甚至写个安卓应用挂个桌面插件,想怎么做都可以。我这里就单纯将数据接入 qqbot 扔到了宿舍群,简单写了个关键词触发。

2024/9/24
articleCard.readMore

使用 Caddy 反向代理 dockerhub 需要几步?

几个月前,由于众所周知的原因,中国大陆境内失去了所有公共的 dockerhub 镜像(或者说是反代)。网上随即涌现了一批自建 dockerhub 反代的,有用 Cloudflare Workers 的,也有用 nginx 的,甚至还有自建 registry 的。 我使用 caddy 去反代 dockerhub 的原因很简单,一是配置简单,二是通过一台国内访问质量良好的境外服务器进行反向代理的访问质量会比 Cloudflare 减速器好很多。 在网上一阵搜索后,并没有发现任何使用 caddy 去反向代理 dockerhub 的文章, 于是本文应运而生。 遇事不决先抓包 为了弄清楚 docker 从 dockerhub 拉取镜像的过程,需要先对网络请求进行抓包。具体的抓包方案我使用的是 mitmproxy,手动信任 ssl 证书的操作在「在 Linux 下使用 mitmproxy 抓取 HTTPS 流量」这篇文章中已经讲过了,只需要配置 dockerd 使用本机的 8080 端口进行代理即可。 docker pull 时,是调用 dockerd 进行镜像拉取,而 dockerd 在绝大多数发行版上都是由 systemd 进程直接启用了,在 shell 中直接设置环境变量的方式并不能进行代理,而透明代理的方案会引入大量无关请求,增加流量分析的难度。 比较好的方案是直接在 systemd 服务这一层设置好代理的环境变量,我这里参考的是「配置 HTTP/HTTPS 网络代理 | Docker — 从入门到实践」这篇文章。 $ cat /etc/systemd/system/docker.service.d/http-proxy.conf [Service] Environment="HTTP_PROXY=http://127.0.0.1:8080" Environment="HTTPS_PROXY=http://127.0.0.1:8080" 重启完 systemd 服务,万事俱备,我拉取了一个较小的 docker 镜像,顺利得到了预期的结果。 docker pull svenstaro/miniserve:latest docker 先请求了 registry-1.docker.io 得到了 401 的 http 状态码后转去访问了 auth.docker.io,得到了 Authorization 字段以后重新请求 registry-1.docker.io,获取源数据后被 307 转发到了 production.cloudflare.docker.com 上。 其中,第一个 401 响应的响应头中,用 WWW-Authenticate 字段标注了 auth 鉴权的域 而 307 响应的响应头中,使用 Location 字段标注了被转发到的 url 三个域名都需要反向代理嘛? 首先,作为我们提供反代服务的入口,registry-1.docker.io 一定是需要代理的,否则就无法提供反代后的服务。 auth.docker.io 只出现了一次,需要反代嘛?根据它在境内的访问质量,恐怕是需要反代的。 最后就是 production.cloudflare.docker.com ,这也是我们最终下载镜像文件的地方,99% 以上的流量都是打到这里去的,而 cloudflare 在境内的访问质量是知名的减速器,完全不可以信赖。 因此,三个域名都需要反代。 如何反代 分三个域名各自代理,在 registry-1.docker.io 那一块进行特殊处理,将响应头中的 WWW-Authenticate 和 location 字段进行关键词替换,将原域名替换为反代域名。 最后的成果大概就是这个样子: dockerhub.example.com { reverse_proxy https://registry-1.docker.io { header_up Host {http.reverse_proxy.upstream.hostport} header_down WWW-Authenticate "https://auth.docker.io" "https://auth.dockerhub.example.com" header_down Location "https://production.cloudflare.docker.com" "https://production.dockerhub.example.com" } } auth.dockerhub.example.com { reverse_proxy https://auth.docker.io { header_up Host {http.reverse_proxy.upstream.hostport} } } production.dockerhub.example.com { reverse_proxy https://production.cloudflare.docker.com { header_up Host {http.reverse_proxy.upstream.hostport} } } PS: 推荐后两个域名使用 CNAME 解析到第一个域名,这样后面更改解析的时候更方便一些。 如何设置 docker 使用反代 可以直接在 docker pull 和 docker run 的命令前加上域名,比如原本的 docker run hello-world 改成 docker run dockerhub.example.com/library/hello-world (如果原本的镜像由 dockerhub 官方提供,没有用户名,路径需要加上 “library”) 也可以选择以前的方案,创建或修改 /etc/docker/daemon.json: sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": [ "https://dockerhub.example.com" ] } EOF sudo systemctl daemon-reload sudo systemctl restart docker 验证 一般来说,能够在中国大陆境内的网络质量下较快地下拉镜像本身就代表反代成功了,但保险起见可以像本文的第一部分一样抓个包,看看是不是都走了自己的域名了。 参见 国内的 Docker Hub 镜像加速器,由国内教育机构与各大云服务商提供的镜像加速服务 无障碍访问 Docker Hub 的各种方法(自建 registry、Cloudflare 加速、Nginx 反代、代理 Docker 网络) | 绅士喵

2024/9/21
articleCard.readMore

将 Rustdesk 中继服务从 Arch Linux 迁移至 Debian

这次迁移主要是两方面原因,一来是我安装了 Arch Linux 的 VPS 要过期了,续费价格过高,没有续费的动力;二来是手上的 VPS 越来越多,逐渐意识到 Arch Linux 作为滚动发行版,每次安装新的软件都要 Syu 甚至重启系统,实在没有太多的精力去维护,这也是为什么 Arch Linux 仅适合桌面发行版。 原本在 Arch Linux 上部署的 rustdesk server 我是按照这篇文章「(水文)在archlinux上部署rustdesk服务端」部署的。本身没什么技巧,直接从 AUR 安装现成的 rustdesk-server-bin,使用 systemctl 启用 rustdesk-server-hbbr.service 和 rustdesk-server-hbbs.service 两个服务即可。 Rustdesk 现在为 Debian 提供了官方的中继服务器的 deb 包,而谷歌搜了一圈都是下载 zip 包使用 pm2 管理进程,故写下此文。 备份原服务器的 rustdesk 密钥 AUR 上的安装方案将密钥放在 /opt/rustdesk-server/data 直接用 sftp 获取 id_ed25519 和 id_ed25519.pub 两个文件就行。如果是新部署的没有这两个文件也没事,rustdesk 服务在启动时可以自动创建,只不过需要在客户端重新输入公钥。 sftp> get /opt/rustdesk-server/data/id_ed25519 sftp> get /opt/rustdesk-server/data/id_ed25519.pub 在新服务器上下载 deb 包,进行安装 apt install -y curl jq version=$(curl -s https://api.github.com/repos/rustdesk/rustdesk-server/releases/latest | jq .tag_name) hbbr_deb=rustdesk-server-hbbr_${version:1:-1}_amd64.deb hbbs_deb=rustdesk-server-hbbs_${version:1:-1}_amd64.deb utils_deb=rustdesk-server-utils_${version:1:-1}_amd64.deb for deb in $hbbr_deb $hbbs_deb $utils_deb do curl -L https://github.com/rustdesk/rustdesk-server/releases/download/${version:1:-1}/${deb} -o ${deb} done dpkg -i $hbbr_deb $hbbs_deb $utils_deb rm $hbbr_deb $hbbs_deb $utils_deb 简单写了个脚本,仅适用 amd64,也没做异常处理,如果服务器在大陆境内需要自行解决 github 下载时可能出现的网络波动问题。 dpkg 安装结束后默认会启用两个 systemd 服务并开机自启,所以不需要使用 systemctl 手动启用。 替换密钥 将刚刚备份的一个公钥和一个私钥放在 Debian 服务器的相应路径,问题是这个路径在哪里呢? 通过翻看 rustdesk 的 service 文件,我们大概可以定位到是在 /var/lib/rustdesk-server/ 路径下的 直接对两个密钥文件进行替换,重启 rustdesk 相关的两个 service 服务即可。 开放服务器防火墙 需要开放如下端口,记得 Linux 的防火墙和云服务供应商面板(如果有的话)上都要开放 TCP(21115, 21116, 21117, 21118, 21119) UDP(21116) 客户端设置 id_ed25519.pub 对应客户端中需要输入的 Key,大概长成下面这个样子 rdtxujYccRLXwXOu2KR3V9cGgP51lEdSmE0HJHGNkn4= ID 服务器直接输入中继服务器的 ip 或者解析到对应 ip 的域名即可,另外两个地址可以不填,RustDesk会自动推导(如果没有特别设定) 成果展示 参见 Installation :: Documentation for RustDesk RustDesk Debian 自建中继服务器 (水文)在archlinux上部署rustdesk服务端

2024/9/20
articleCard.readMore

自建图床小记五——费用

自建的图床自 8 月 13 日正式启用以来,已经过去一周多了,具体的费用是多少呢?原先设计的 0 额外投入有没有实现呢? 这是我的博客访问统计,在这一周多的时间内,一共有 1.27k 次页面访问,被 671 个访客访问了 769 次,平均下来每天也有一百多次的页面访问。 Cloudflare Workers 和 Cloudflare R2 的免费额度全部够用,用量全部小于免费额度的 1%。 又拍云联盟每年可以领取 67 元的代金券,平均每天控制在 0.18 元内即可实现白嫖。 可以看到,这一套图床在我博客当前和可见的未来的访客情况下,在不被人恶意刷流量的情况下,是不需要投入除域名续费以外的其他成本的。

2024/8/21
articleCard.readMore

自建图床小记四——上传脚本编写与图片迁移

前面三篇小记分别讲述了图床的整体架构、用 Workers 构建 Restful API 和 自动更新部署 SSL 证书,这一篇c处理由此带来的图片上传问题,主要是要为 Typora 编写自动上传脚本,并为博客原有的图片进行迁移。 自动上传脚本 主要还是给 Typora 用,实现这种效果 #!/bin/bash HOST="upload.example.com" CDN_HOST="cdn.example.com" UPLOAD_PATH="uploads/$(date +%Y/%m/%d)" AUTH_TOKEN="1145141919810" webp=false markdown=false force=false keep=false while getopts ":mwfkp:" opt; do case $opt in m|markdown) markdown=true ;; w|webp) webp=true ;; f|force) force=true ;; k|keep) keep=true ;; p|path) UPLOAD_PATH=$OPTARG ;; \?) echo "Invalid option: -$OPTARG" ;; esac done shift $((OPTIND - 1)) UPLOAD_URL="https://$HOST/$UPLOAD_PATH" if [[ "$UPLOAD_URL" == */ ]]; then UPLOAD_URL="${UPLOAD_URL%?}" fi for image in "$@"; do if [ "$webp" = true ]; then cwebp -quiet "$image" -o "${image%.*}.webp" image="${image%.*}.webp" fi if [ "$keep" = true ]; then FILENAME=$(basename "$image") else FILENAME="$(md5sum $image | cut -c 1-13).$(basename $image | cut -d. -f2)" fi if [ "$force" = true ]; then UPLOAD_RESPONSE=$(curl -s -X PUT "${UPLOAD_URL}/$FILENAME" \ -w "%{http_code}" \ --data-binary @"$image" \ -H "X-Custom-Auth-Key: $AUTH_TOKEN" \ -H "Overwrite: true" \ ) else UPLOAD_RESPONSE=$(curl -s -X PUT "${UPLOAD_URL}/$FILENAME" \ -w "%{http_code}" \ --data-binary @"$image" \ -H "X-Custom-Auth-Key: $AUTH_TOKEN" \ ) fi UPLOAD_HTTP_CODE=$(echo "$UPLOAD_RESPONSE" | tail -n1) if [ -n "$UPLOAD_PATH" ]; then CDN_URL="https://$CDN_HOST/$UPLOAD_PATH/$FILENAME" else CDN_URL="https://$CDN_HOST/$FILENAME" fi if [ "$UPLOAD_HTTP_CODE" != "200" ]; then echo "上传失败: $UPLOAD_RESPONSE" continue fi if [ "$markdown" = true ]; then echo "![](${CDN_URL})" else echo "${CDN_URL}" fi done 这一次使用 Cloudflare Workers 构建的 Restful API 很有意思,使用了 GET、PUT 和 DELETE 三个请求类型。GET 请求很常见,是用来获取图片的,PUT 和 DELETE 在 web 开发就不如 GET 和 POST 常见了,这一次也是让我体会到了这两个 http verb 在 Storage Bucket 操作中是有多么形象了。 PUT - 从直观上来讲,就是将某个文件放到目标位置 打个比方,我向 https://cdn.example.com/img/avatar.webp 打了一个请求,并带上了要上传的文件,那就意味着我将这个文件放到了 Storage Bucket 的 /img/avatar.webp 这个位置,所以我在上传后,应该就能用 GET 请求我刚才 PUT 的那个 URL 获取我刚才上传的东西。如果那个路径存在文件,那么默认行为是直接覆盖。 DELETE - 删除目标路径的文件 和 PUT 一样,我在请求对应 URL 后,Storage Bucket 中对应 URL 路径的资源应该被删除。 PUT 和 DELETE 这两个 Http Verb 让我们更像是在对一个真实的文件系统进行操作,而非那种传统的使用 POST 上传的图床那样,我们并不通过 POST 请求上传一个文件,然后获取资源最终被放置位置的 URL —— 我们自己决定资源被存放的位置。 在这个 Shell 脚本中,引入了四个可选选项 m|markdown) markdown=true ;; w|webp) webp=true ;; f|force) force=true ;; k|keep) keep=true ;; p|path) UPLOAD_PATH=$OPTARG ;; markdown 选项决定返回值是否以 ![]() 这种 URL 格式返回 webp 决定上传过程中是否将图片转为 webp 后再上传 force 决定如果遇到文件路径冲突,是否强制覆盖云端的文件 keep 决定是否保留文件原有的文件名进行上传 path 决定文件具体被存放的路径(或者使用默认的路径) HOST 是图床用于上传的地址,CDN_HOST 是图床用于被方可访问的地址。 由于急着用,也没考虑协程的处理方式,等等看后期有没有时间用 Python 重写吧。 博客图床迁移脚本 因为只用一次,所以也没使用协程或者多线程的方式去上传文件——毕竟图片不多,也就两三百张。 import os import re import requests # 哪些后缀的文件需要检测是否存在老图床的 URL 并进行迁移? file_extension = [ '.md', '.yml', '.html' ] pic_urls = [] _files = [] # 用于匹配老图床的正则表达式,这里是按照 lsky pro 的格式编写的 pattern = r'https://cdn.example.com/\d{4}/\d{2}/\d{2}/[a-z0-9]{13}\.[a-z]{3,4}' # 图片的上传部分,需要先从原 url 中下载图片,在上传到新图床中,如果需要的话可以在中途转换为 webp 格式 def upload(url): """ 此处的返回值应该是新的 url """ # 遍历目标后缀文件名的文件,如果存在老图床的 url,则将 url 加入到 pic_urls 列表中,并将这个文件的文件名(相对路径)添加到 _files 列表中 for root, dirs, files in os.walk("."): for file in files: if file.endswith(tuple(file_extension)): file_name = os.path.join(root, file) with open(file_name, 'r') as f: content = f.read() urls = re.findall(pattern, content) if urls: pic_urls.extend(urls) _files.append(file_name) # 先转为集合,再转回列表,进行去重 pic_urls = list(set(pic_urls)) print("共找到图片:", len(pic_urls)) url_dict = {} # 将列表中的图片进行上传,每张图片最多尝试三次上传,如果三次都失败,则保留原连接 for i,u in enumerate(pci_urls, start=1): for t in range(1,4): try: new_u = upload(u) continue except: if t == 3: new_u = u print(f"{u} 无法上传:{e}") url_dict[u] = new_u print(f"{i} / {len(pic_urls)}") # 对 _files 列表中的文件一一完成替换 for file in _files: with open(file, 'r') as f: content = f.read() for k, v in url_dict.items(): content = content.replace(k, v) with open(file, 'w') as f: f.write(content) print("完成替换:", file)

2024/8/20
articleCard.readMore

自建图床小记三—— SSL 证书的自动更新与部署

为什么要自动更新? 众所周知,为站点开启 https 访问需要获得对应 host 的 ssl 证书,而如果希望证书被访客的浏览器所信任,需要拿到由 Certificate Authority (CA) 签发的 ssl 证书。在前一阵子那波 BAT 等大厂提供的云服务停止发放免费的由 TrustAsia/DigiCert 签发的一年有效期免费 ssl 证书之后,市面上已经没有被广泛信任的 CA 签发的免费的一年有效期的 ssl 证书了,于是不得不用回由 Let's Encrypt/ZeroSSL 等 CA 签发三个月免费证书。 但话又说回来,三个月有效期确实不太够,一年有效期的证书就一年一更,手动申请部署也不麻烦;三个月有效期的证书手动就有点麻烦了——我一般会在证书到期的前 15 天进行更新,防止最后几天自己太忙了没时间管。 这套图床架构的自动更新有没有困难? 境外 通过 Cloudflare SaaS 接入的域名通过验证后会自动获得由 Cloudflare 提供的由 Google Trust Services 签发的证书,不需要我们操心。 境内 咱选用的又拍云 CDN 提供了免费的 Let's Encrypt 证书及其自动续期服务,但需要我们把图床访问域名的 DNS CNAME 解析到他们家。 这里有个问题,我们这套图床架构在境外的解析是解析到 Cloudflare 的,不可能通过 Let's Encrypt 的 acme challenge。如果使用 upyun 申请 ssl 证书,则意味着每次更新都要我们手动将境外的 dns 解析记录暂时解析到又拍云,待证书更新成功后再解析回 Cloudflare,非常麻烦。 使用 Github Action 跑 acme.sh 获取 ssl 证书 本着「能使用长期免费稳定服务就使用长期免费稳定服务」的思想,决定使用 Github Action 申请 ssl 证书。 在 Github Action 跑 acme.sh 获取 ssl 证书意味着不能使用 http 文件检验的方式检验域名所有权,需要使用 dns 检验。截至本文写作时间,acme.sh 已经支持了 150+ 个主流的 DNS 解析商(Managed DNS providers)的 api,针对不支持 api 修改 dns 解析记录的,还可以使用 DNS alias 模式——即将需要申请 ssl 证书的域名先 cname 到一个工具人域名上,将工具人域名通过 NS 解析到 acme.sh 支持的 DNS 解析商,进而实现 CA 对域名所有权的验证。 先在本地跑起来 我采用的是 Cloudflare,直接在个人资料页创建一个具有编辑 DNS 权限的 API 令牌 随后在自己的域名页面,找到区域 ID 和 账户 ID 在自己的本机安装 acme.sh,设置好 Cloudflare DNS 的几个变量 export CF_Token="" export CF_Account_ID="" export CF_Zone_ID="" 随后可以尝试使用 acme.sh 签发 ssl 证书 acme.sh --issue --dns dns_cf -d cdn.example.com 上 Github Action 原本是打算直接用 Menci/acme 这个 Action的,可惜遇到了点问题。 在我本地,Cloudflare 相关的 Token 和 ID 并没有被写入到 account.conf,而是被写在 cdn.example.com_ecc/cdn.exampe.com.conf,大概就没办法直接用这个 Action 了,不得不转去手搓。不过好在 Menci/acme 中还是能抄到不少的。 压缩本地的 ca 文件夹 cd $HOME/.acme.sh/ && tar cz ca | base64 -w0 安装 acme.sh - name: Install acme.sh run: curl https://get.acme.sh | sh 解压 ca 文件夹 - name: Extract account files for acme.sh run: | echo "${{ secrets.ACME_SH_ACCOUNT_TAR }}" | base64 -d | tar -C ~/.acme.sh -xz 执行 acme.sh 申请证书 - name: Issue Certificate run: | export CF_Token="${{ secrets.CF_TOKEN }}" export CF_Zone_ID="${{ secrets.CF_ZONE_ID }}" export CF_Account_ID="${{ secrets.CF_ACCOUNT_ID }}" mkdir -p output ~/.acme.sh/acme.sh --issue --dns dns_cf --force -d ${{ env.domain }} --fullchain-file output/fullchain.pem --key-file output/key.pem 压缩证书 - name: zip Certificate run: | zip -j output/${{ env.domain }}_$(date +%Y%m%d).zip output/fullchain.pem output/key.pem 通过 tg bot 发送压缩包给自己 - name: Push Certificate run: | TG_BOT_TOKEN="${{ secrets.TG_BOT_TOKEN }}" TG_CHAT_ID="${{ secrets.TG_CHAT_ID }}" curl -s -X POST https://api.telegram.org/bot${TG_BOT_TOKEN}/sendDocument -F chat_id=${TG_CHAT_ID} -F document="@output/${{ env.domain }}_$(date +%Y%m%d).zip" 部署到又拍云 这里使用的是 menci/deploy-certificate-to-upyun。由于又拍云没有提供上传 ssl 证书的 api,因此只能通过模拟用户登陆的方式实现。 - name: Deploy To Upyun uses: Menci/deploy-certificate-to-upyun@beta-v2 with: subaccount-username: ${{ secrets.UPYUN_SUBACCOUNT_USERNAME }} subaccount-password: ${{ secrets.UPYUN_SUBACCOUNT_PASSWORD }} fullchain-file: output/fullchain.pem key-file: output/key.pem domains: | ${{ env.domain }} delete-unused-certificates: true 参见 使用 GitHub Actions 自动申请与部署 ACME SSL 证书 (续)acme.sh脚本使用新cloudflare api令牌申请证书 acmesh-official/acme.sh

2024/8/14
articleCard.readMore

自建图床小记二——使用 Workers 为 R2 构建 Restful API

访问 R2 的两种方式 一般来说,想要访问 Cloudflare R2 中的文件,会有两种方式。 一种是在 R2 的设置界面设置自定义域 另一种是通过 Cloudflare Workers 进行访问 那么应该选择哪种?选择 Cloudflare Workers! 为什么是 Cloudflare Workers? 要回答这个问题比较困难,但可以回答另一个问题——「为什么不设置自定义域实现直接访问?」 自定义域的访问存在限制 设置自定义域的访问方式存在较多的限制,让我们先来复习一下上一篇博客中提到的 DNS 解析方案 1 在这里,我们需要将图床访问域名通过 NS 接入 DnsPod 实现境内外的分流,但 R2 所允许设置的自定义域必须是通过 NS 接入 Cloudflare 的,这存在冲突。那如果我们先将自定义域设置为通过 NS 接入 Cloudflare 的工具人域名,再将图床访问域名通过 CNAME 解析到工具人域名会不会有问题呢?恭喜你获得 403 Forbidden。 如果通过上一篇文章中的 DNS 解析方案 2 来进行 DNS 解析,能不能成功设置为 Cloudflare R2 的自定义域呢?也不行,Cloudflare R2 的自定义域会占用域名的解析,这意味着你无法将图床访问域名解析到用于分流的工具人域名。 结论:截至本文写作时间,设置自定义域的方案不适用于 DNS 分流的图床架构。 如何上传文件到 Cloudflare R2? 网页端直接上传 最简单的上传方式是直接在 Cloudflare 进行网页上传,但这种方案不适合自动化脚本,也没法接入 Typora 使用 Amazon S3 的兼容 API 手动调用 S3 API Cloudflare R2 被设计为兼容 Amazon S3 的存储方案,自然兼容 Amazon S3 的上传 API,在 Cloudflare Docs 中有关于 S3 API 的实现情况记载,大部分接口功能都是实现了的。但。。。但 S3 使用的是 AWS Signature 作为鉴权,你不会希望在每个自动化程序中都自己实现一次的。。。 使用 aws-cli 等 SDK 使用 aws-cli 可以自动实现计算 AWS Signature,这是一种可行的方案,但我可能会在别的服务中使用到我的图床,不是所有的服务所处的环境都能够执行 shell 命令,也不是所有的编程语言都有现成的 SDK 可用。 使用 Cloudflare Workers 构建 Restful API 在 Cloudflare Docs 中明确提出可以使用 Cloudflare Workers 访问 Cloudflare R2 Bucket,通过 Workers 设置界面的按钮,可以非常方便的将 R2 Bucket 作为一个 R2Object 绑定到 JavaScript 的一个变量中,这里有相关的开发文档。 结论: 从易用性上来看,使用 Cloudflare Workers 构建 Restful API 这种上传文件的方案是最为合适的。 使用 Cloudflare Workers 构建 Restful API 的方案有没有什么缺点? 有。 Cloudflare Workers 的每日额度是有限的,在极端的流量下可能会用完(应该不会吧?) Cloudflare Workers 的内存限制为 128MB,在上传下载 > 100MB 的文件时可能会出错。有这种体积上传需求的场景建议使用别的上传方案。 如何构建? 直接贴代码 const hasValidHeader = (request, env) => { return request.headers.get('X-Custom-Auth-Key') === env.AUTH_KEY_SECRET; }; function authorizeRequest(request, env, key) { switch (request.method) { case 'PUT': case 'DELETE': return hasValidHeader(request, env); case 'GET': return true; default: return false; } } export default { async fetch(request, env) { const url = new URL(request.url); const key = decodeURI(url.pathname.slice(1)); if (!authorizeRequest(request, env, key)) { return new Response('Forbidden\n', { status: 403 }); } switch (request.method) { case 'PUT': const objectExists = await env.MY_BUCKET.get(key); if (objectExists !== null) { if (request.headers.get('Overwrite') !== 'true') { return new Response('Object Already Exists\n', { status: 409 }); } } await env.MY_BUCKET.put(key, request.body); return new Response(`Put ${key} successfully!\n`); case 'GET': const object = await env.MY_BUCKET.get(key); if (object === null) { return new Response('Object Not Found\n', { status: 404 }); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set('etag', object.httpEtag); return new Response(object.body, { headers, }); case 'DELETE': await env.MY_BUCKET.delete(key); return new Response('Deleted!\n'); default: return new Response('Method Not Allowed\n', { status: 405, headers: { Allow: 'PUT, GET, DELETE', }, }); } }, }; 代码的大部分都是基于 Cloudflare Docs 中给出的样例,修改了几个小的优化点 删除了 ALLOW_LIST 部分代码,默认所有文件都是可以被访问的 在上传一个文件时,如果目标路径存在同名文件,则不直接覆盖,而是返回 409 的异常 HTTP 相应,如果想要强制覆盖,则需要在 Http Header 中加入 Overwrite: true 解出请求路径时,使用 decodeURI( ) 方法先进行解码,解决文件路径中含有中文时会导致请求失败的问题。 填入代码后,还需要绑定两个变量,一个是 R2 Bucket 另一个是自己的管理密码 如何使用 Cloudflare Workers 构建的 Restful API 进行文件操作? 上传 以 python 为例,上传一个文件 1MB.bin 到 /example/ 目录下,上传的 url 就是文件最终的存在路径。 import requests AUTH_KEY_SECRET='1145141919810' with open('1MB.bin', ''rb) as f: file_content = f.read() requests.put( 'https://r2.example.workers.dev/example/1MB.bin', headers={ 'X-Custom-Auth-Key': AUTH_KEY_SECRET, 'Overwrite': True # 如果不需要强制覆盖可以删除这一行 } ) 访问 通过浏览器直接访问 https://r2.example.workers.dev/example/1MB.bin 应该就能访问到 删除 仍然以 python 为例,删除刚才的文件 import requests AUTH_KEY_SECRET='1145141919810' with open('1MB.bin', ''rb) as f: file_content = f.read() requests.delete( 'https://r2.example.workers.dev/example/1MB.bin', headers={ 'X-Custom-Auth-Key': AUTH_KEY_SECRET } ) 参见 用 cloudflare 的 R2 和 worker 来做文件托管 Workers API reference Use R2 from Workers 创建已签名的 AWS API 请求

2024/8/13
articleCard.readMore

自建图床小记一——图床架构与 DNS 解析

一直以来,我使用的都是使用付费的第三方图床,可惜最近几年为了节省成本,境内的稳定性出现了一些问题。过去一年中光是我本人遇到的无法访问的情况就有三四次,其中两次持续时间超过 2 小时,甚至有网友特意来 at 我告知我博客使用的图床出问题了,还有两次是在我作品验收前 24 小时内出现,幸亏我及时切换了资源链接。此外,境外 CDN 也从原先的 Cloudflare 换掉了,目前海外的解析结果似乎只有一个在美国的节点,其余地区(尤其是日本香港新加坡等常用的落地地区)的访问质量不佳,Google 的 page speed test 甚至提示我的图片拖慢了网站加载速度。 基于上述种种原因,我开始选择自建图床,前前后后折腾了快一周后,新图床终于投入使用,目前我的博客已经完成了所有图片资源的切换。 架构设计 这一套架构使用 Dnspod 免费版实现在境内外的解析分流,将境内的流量导向又拍云 CDN 为境内的访客提供服务,在境外使用量大管饱的 Cloudflare CDN 节省成本,为全球提供加速访问。 为什么是又拍云 如你所见,我的博客底部挂了又拍云的 logo。又拍云联盟为个人开发者提供了每个月 10GB 存储和 15GB 的免费 CDN 流量,在每年通过申请后会以 67 元无门槛代金券的形式发放到账号,也不用担心某个月超了一点点而付出额外的费用。 相比之下,七牛云虽然控制台的前端 UI 不错,但出了这种事情导致其在我心里印象分极差: 「从山西联通到组播IP:七牛云的奇怪视角(附分析和后日谈)」Archived Here 为什么是 Cloudflare R2 作为自己的图床,必须要保证稳定性,境内访问的稳定性可以先放到一边,最重要的就是保证源文件的稳定性。不同于在自己的 VPS 上存储图片的方案,使用 Cloudflare R2 作为储存不需要关注 VPS 到期以后的图片迁移问题。使用 Cloudflare R2 作为储存,免费用量对于个人站点来说绰绰有余,在 10GB 存储容量超出之前不用考虑别的问题,也不用担心资金支持不下去导致的麻烦。而不使用又拍云提供的 10GB 存储也可以节省这部分的代金券金额,让代金券尽可能多的抵扣境内 CDN 流量带来的费用。 需要的东西 两个或两个以上的域名(其中一个需要备案) Cloudflare 所支持的境外支付方式(PayPal 账号 / Visa Card / Master Card),用于开通 Cloudflare R2 和 Cloudflare SaaS 接入 很多很多钱(其实没有很多,又拍云联盟每年的 67 元抵用券在我这里看来完全是够用的) 聪明的大脑,能够快速敲击键盘的双手,能够支持你熬夜的心脏 * 在这一套架构中引入了香港 VPS 进行反向代理,一来是防止国内 CDN 与 Cloudflare 的网络连接质量过差导致的回源失败,二来也是方便我在没有国际联网的情况下进行图片的上传,但如果没有条件其实是可以去掉的。 DNS 解析 如上图,将图床域名 NS 接入 DnsPod,工具人域名 NS 接入 Cloudflare 即可实现境内外分流的效果。 图床访问域名在境外 CNAME 解析到工具人域名 图床访问域名在境内 CNAME 解析到境内 CDN 服务商 工具人域名在 Cloudflare 上解析到任何站点都行,只需点亮解析时 Cloudflare CDN 代理按钮即可生效。 但如果你的备案域名已经通过 NS 接入了 Cloudflare,可以采用下面这套架构。 * 解析方案 2 中的图床访问域名和工具人域名可以是同属于同一二级域名的不同子域名 这种方案要多一步,把图床访问域名 CNAME 解析到用于分流的工具人域名。 Cloudflare SaaS 接入 SaaS 接入大概就是如图所示,此外还要配置 Cloudflare Workers 的域名访问 这样就能保证在境外访问图床域名时将请求打到 Cloudflare Workers 上了,关于使用 Cloudflare Workers 构建图床 Restful API 相关的内容我放在下一篇博客讲。 参见 图床 CDN CNAME 接入 Cloudflare SaaS 实现分流

2024/8/12
articleCard.readMore

在 Linux 下使用 mitmproxy 抓取安卓手机上的 HTTPS 流量

纵使安卓下有小黄鸟 HttpCanary 这种抓包神器,但手机一块 6 英寸的小屏实在是不方便分析流量情况,还得是 PC 的屏幕更大一些,处理起流量信息来更得心应手一些。 把话说在前面,目前的安卓抓包有不小的限制 Android 7 以下的版本: 直接以普通用户的权限安装 ssl 证书即可被信任 Android 7 以上的版本: 安全性较低的应用: 需要使用 root 权限将证书移动至 /system/etc/security/cacerts使证书被系统信任 安全性较高的应用(比如微信 7.0 以上的版本): 在满足上一条条件的情况下,需要阻止第三方应用使用自带的 ssl 证书信任范围(绕过 SSL Pinning)。通常情况下需要额外的手段对目标应用进行篡改,比如使用 justTrustMe 这个 xposed 模块,或者 frida。 除此之外,Linux 版本 >= 5.5 的安卓设备也可以使用 eCapture 这款基于 eBPF Linux 内核模块实现的抓包软件,算是种奇技淫巧。 本文只讨论 Android 7 以上版本中安全性较低的应用,因为我当前的抓包目标局限于一款安全性不高的外包软件。 基本操作 见「在 Linux 下使用 mitmproxy 抓取 HTTPS 流量」 安装 ssl 证书 cp $HOME/.mitmproxy/mitmproxy-ca-cert.pem $(openssl x509 -subject_hash_old -in $HOME/.mitmproxy/mitmproxy-ca-cert.pem | head -n 1).0 此时我们就可以在家目录下找到一个以 .0 结尾的证书文件,我们的目标是将其放到手机的 /system/etc/security/cacerts 路径下。 对于一些出厂安卓版本较低、system 分区采用可变文件系统的手机,我们可以很轻松的使用带有 root 权限的文件管理器将证书文件移动到对应的目录(我这里就是);而对于出厂版本较高的手机,system 分区可能是不可写的,需要采用额外的奇技淫巧。 1、通过 ADB 将 HTTP Toolkit CA 证书推送到设备上。 2、从 /system/etc/security/cacerts/ 中复制所有系统证书到临时目录。 3、在 /system/etc/security/cacerts/ 上面挂载一个 tmpfs 内存文件系统。这实际上将一个可写的全新空文件系统放在了 /system 的一小部分上面。 将复制的系统证书移回到该挂载点。 4、将 HTTP Toolkit CA 证书也移动到该挂载点。 5、更新临时挂载点中所有文件的权限为 644,并将系统文件的 SELinux 标签设置为 system_file,以使其看起来像是合法的 Android 系统文件。 ——《安卓高版本安装系统证书 HTTPS 抓包 - 终极解决方案》 「archived here」 让被抓包的应用流量经过 mitm 代理服务器 mitmproxy 默认会在 pc 端的 8080 端口开启一个 http 代理服务器,我们要做的就是想办法让待抓包的应用流量被这个 http 代理服务器所代理。 [zhullyb@Archlinux ~]$ ip -br a lo UNKNOWN 127.0.0.1/8 ::1/128 enp0s31f6 UP 172.16.0.255/25 fe80::2df9:2927:cd44:65c/64 wlp0s20f3 UP 192.168.20.212/24 fe80::a6bc:919:281e:dcab/64 docker0 DOWN 172.17.0.1/16 fe80::42:d1ff:febe:d513/64 在这里我们能看到本机的无线网卡地址是 192.168.20.212,所以 http 代理服务器的地址就是 http://192.168.20.212:8080 。(如果你的有线网卡和手机在同一局域网下,当然也可以用有线网卡的 ip 地址) 我们当然可以在安卓手机的 WIFI 连接页面填入 http 代理地址。 但这对我来说似乎并不是一个好主意:一来并不是所有的应用都会默认使用 http 代理服务器,二来这回导致抓包目标不明确,非目标应用的流量也会经过代理服务器。 我选择了 Nekobox 这个常见的代理软件,它支持 http 代理服务器,且允许分应用代理。 可以看到能正常抓取 https 流量 参见 安卓应用防抓包机制及一些绕过 安卓7.0+系统抓包方案 frida抓包 gojue/ecapture 安卓高版本安装系统证书 HTTPS 抓包 - 终极解决方案

2024/7/31
articleCard.readMore

为中柏 N100 小主机开启来电自启

因为收到通知,寝室过两天要断电 20 分钟,所以需要打开 N100 家里云的来电自启功能。 正常关机短暂等待数秒后,开机,狂按 Delete 键进入 BIOS。 在 Advanced 选项中选择「OEM Configuration」 可以在最后一行「AC Power Loss」中选择模式。 Power Off: 关闭相关功能。 Power On: 传统意义上的来电自启,只要接通电源就会自启动。 Last State: 只有在上次关机是意外断电导致时,接通电源才会自启动。

2024/7/22
articleCard.readMore

我的博客被完整地反向代理,并自动翻译成了繁体中文

2024.08.20更新 我将境外的 Github Pages 解析停了,所有流量全部指向我的 HK 的 vps。 访问对方站点 /?about/ 时,在我服务器 /about/ 收到了一个奇怪的请求,访问对方别的路径时也会在我服务器的对应路径收到请求,UA 伪装成了 Google 家的爬虫: (关于为什么有 Mozilla 字段,可以参见 《是的,所有现代浏览器都假装自己是火狐》) 这个 ip 的归属地是新加坡 Cogent,合理怀疑是对方的源站 IP(也有可能只是对方用于请求的爬虫 ip)。直接通过 ip 访问对方站点,发现是 lnmp 的安装成功提示: 我注意到对方站点在 html 结尾处加了如下字段 <!-- freevslinks --><div style="display:none"><a href="http://www.xxfseo.com/?time=1721267439">xxfseo.com</a></div><!-- /freevslinks --> 似乎是专业产生互联网垃圾的组织。 我目前已经屏蔽了来自 154.39.149.128 这个 ip 的访问请求,对方的站点暂时性崩盘,以后可能会换用别的 ip 来爬也说不准,先到此为止吧。 现象 今早打开我的流量统计网站,发现我的博客有一个神奇的 referer 顶着我博客用的 favicon,但竟然不是我的域名。点进去一看,发现我的博客被翻译成了繁体中文,而且语句读上去也不是很通畅。Archived here. 再打开关于页一看,把我的博客域名给干掉了,只留下一个反代域名。Archived here. 随机打开一个幸运页面,使用 F12 控制台查看流量情况,发现 umami 统计和 waline 评论都用的我个人部署的 查询 ip 归属地,是老朋友 Cloudflare 泛播 结合 url 上不明所以的问号,推测应该是 cloudflare workers 反向代理 + 调用翻译 api + 关键词替换。我小小更新了某个页面,发现对方站点也立马更新了,基本可以确定是反向代理。 whois 查询没有获得任何有用信息,一眼望去全是隐私保护。 事先声明,我的博客采用CC BY-NC-SA 4.0,我个人是非常欢迎任何人注明出处的情况下搬运甚至翻译我的文章的,甚至允许搬运到 csdn——只要你不开收费访问。但这种反代行为我是非常抵触的。 文章被翻译成了繁体中文,但没有注明是翻译稿,直接把我本人的网名用繁体写了上去,这并不符合 CC BY-NC-SA 4.0 的要求。 翻译质量很差,就连机翻都不应有这种奇怪的同义词替换,问了问熟悉繁中的朋友说是港台也没有这种用法,像是故意洗稿。 反向代理了我的整个网站,但把我关于页上的博客链接给去掉了,我不认为这是善意的反代行为。Archived here. 仍然在使用我的 waline 评论和 umami 统计。 没有给我任何事先的邮件说明或者评论留言,whois 开隐私保护的情况下,我找不到任何方法去联系这位域名的持有者。 怎么办? 植入 js 进行跳转 因为对方同步的及时性很强,高度怀疑是 cloudflare workers 反向代理,且评论和流量统计都直接原模原样用的是我的 js,我就注入一个 js 检测 host,如果不是我的域名或者本地调试时使用的 127.0.0.1 or localhost,则清空页面内容,给出文字提示,五秒后跳转到我的博客。代码如下: const host = window.location.host if (host !== 'zhul.in' && ! host.startsWith('localhost') && ! host.startsWith('127.0.0.1')) { document.body.innerHTML = [ '<div style="margin: auto;">', '<h1>当前页面并非本文作者的主页,将在五秒后跳转。</h1>', '<br />', '<h1>请此站点持有者联系我: zhullyb@outlook.com</h1>', '</div>', ].join('') document.body.style = [ 'background-color: white;', 'color: black;', 'text-align: center;', 'font-size: 50px;', 'width: 100vw;', 'height: 100vh;', 'display: flex;', ].join('') setTimeout(() => { window.location.href = 'https://zhul.in' }, 5000) } 给 waline 和 umami 设置限制 我博客使用的 waline 和 umami 均是我自己在 vercel 上架设的,我自然可以根据访客的 referer 来判断请求的来源。不过看了下,vercel.json 文件并不能直接实现这个需求,可能需要我们自己来编写一些简易的中间件。 Waline waline 文档中有明确提到,waline 基于 Koa 框架开发,可以自行编写中间件。 // example/index.cjs const Application = require('@waline/vercel'); module.exports = Application({ plugins: [ { middlewares: [ async (ctx, next) => { const referer = ctx.request.headers['referer']; if (referer) { if ( !referer.include('localhost') && !referer.include('127.0.0.1') && !referer.include('zhul.in') ) { ctx.status = 403 ctx.body = 'Forbidden' return } } await next(); }, ] } ], async postSave(comment) { // do what ever you want after comment saved }, }); 成效立竿见影 umami 对 umami 的第一次请求是 script.js,这个请求是因为 html 头部添加了 umami 的 script 链接,这一次请求是不带有 referer 的,因此,对方站点使用我的 umami 统计并不会给我的博客访问统计造成错乱——umami 能够自行分辨对方的站点是否是当初添加网站时填写的站点。但我不能忍的地方在与 umami 的数据库会记录对方站点的流量情况,这占用了我的数据库空间。 umami 使用 nextjs 开发,似乎并没有给我留可供自定义的接口,贸然修改源码则可能会在下次 merge 官方代码时遇到麻烦。为了给自己省点事,我选择不再让博客加载 https://umami.zhul.in/script.js ,而是将其中的内容复制保存下来,添加基于 host 的判断条件来决定是否向自建的 umami 服务发起请求。 尝试向 cloudflare 举报滥用行为 cloudflare 是允许提交滥用举报的,这个域名正在使用 cloudflare 提供服务,因此我可以尝试举报,链接在这里: https://www.cloudflare.com/zh-cn/trust-hub/reporting-abuse/ 类别就可以选 DCMA,因为对方没有遵守 CC BY-NC-SA 4.0 协议给我的文章做出合理的署名,且我的博客关于页面不属于 CC BY-NC-SA 4.0 的范畴,对方是没有理由去对这一页做出二次分发的行为的。 不过我暂时还没这么做,我期待着我前面的几个方案能够奏效,我仍寄希望于对方会及时和我沟通,我也不太想为此去填一张额外的烦人的表单。 最终效果

2024/7/18
articleCard.readMore

尝试体验 Fedora COPR 中的 allow SSH 功能

在今年的早些时候,我在 COPR 看到了一个新出现的名为「allow SSH」的按钮。 我在 COPR 的 User Documentation 中找到了对应的描述。 Sometimes it is useful to manually debug failed builds not locally but within the Copr infrastructure. That’s why it is possible to allow SSH access to a copr builder. More information in the SSH access to Copr builders blog post. COPR 的这项功能允许包维护者远程访问自己没有的 CPU 架构或 Linux 发行版的 Linux 环境,大大减少打包时的痛点。 开始使用 尝试点击按钮,获得如下界面,可以填写自己的 ssh 公钥,最多可以选择两台设备,如果选择的设备数量大于 2,则剩下的人物会维持在 pending 状态,直到被你 ssh 连接的构建机完成对应的构建任务。 在该次构建的详情页面,等待 backend.log 按钮出现 在这个 url 对应的文件中,我们可以找到需要的 ssh 命令 使用对应的 ssh 命令即可连上构建服务器 先跑个 neofetch 看看,双核 16G,看着还行。 随手跑了个 speedtest,竟然是千兆上下传对等的网速。 在这台机子上,我们可以使用 builder-live.log 中的命令手动触发一次构建(不过我这里跑了一半就报错了,疑似是系统不够完善) 不过很可惜,COPR 似乎并没有给我们中途去干预/调试构建过程的方案,仅仅是提供了一个可供自由操作的 Linux 环境。使用 copr-rpmbuild 命令可以进行对应的构建,但构建过程依然是在沙箱内进行,且没有给中途暂停/调试的机会。如果需要一步步手动的构建,还是建议使用 rpmbuild 命令进行。 杂项 使用 copr-builder help 命令可以获取打包机的提示信息 使用 copr-builder show 命令查看剩余时间 使用 copr-builder prolong 可以延长打包机的有效时长 使用 copr-builder release 可以销毁当前的打包机环境 限制 由于安全原因,构建结束后,只有 spec 文件和日志可以被存储到 copr 对应项目的服务器。打包机会使用一个独特的沙箱防止其构建产物被二次使用,哪怕是同一个用户都不行。 为了避免资源艾琳娜贵妃,同一用户在同一时刻最多只能使用两台具有 ssh 访问权限的打包机。 由于上面的两套规定,当 copr 构建失败时并不能自动启动 SSH 访问权限,需要用户手动在面板上 resubmit 当前任务并选择使用 SSH 访问权限。 打包机在默认情况下 1 小时后自动销毁,除非你手动申请延长时间,最长为 48 小时。 有些打包机只有 IPv6 的访问地址,你没得选。如果你无法连接 IPv6 网络,你可以取消当前的任务并重新发布并期待能给你下发一台具有 IPv4 访问地址的打包机(其实非常少),或者使用代理。 如果 SRPM 构建失败,则不能 resubmit 当前任务。这是 COPR 的实现逻辑问题,未来可能得到改善。 参考 「SSH access to Copr builders」

2024/7/15
articleCard.readMore

在 Arch Linux 下配置使用 HP Laser 103w 打印机无线打印

我寝室有一台使用 wifi 连接的 HP Laser 103w 打印机,这些天刚好布置了新的 HomeServer,因此来记录一下这台打印机的配置过程,根据 HP 官网驱动包的名字「HP Laser 100 and HP Color Laser 150 Printer series Print Driver」推断,此过程应该能适用于所有的 HP Laser 100 及 HP Color Laser 150 系列的打印机。 打印机联网 首先使用 Windows 操作系统完成打印机的联网工作,在路由器的网页管理界面可以看到这台打印机的局域网 ip 是 192.168.123.20 ,记录备用。如果有条件的话,尽量将打印机的 MAC 地址与 IP 地址绑定,避免路由器将该 IP 分配给别的设备。 安装 CUPS 随后按照 ArchWiki 的 CUPS 页面进行相关配置,CUPS 是苹果公司开源的打印系统,是目前 Linux 下最主流的打印方案。 首先安装 cups ,如果需要「打印为 pdf」的功能,可以选装 cups-pdf。 pacman -S cups pacman -S cups-pdf 接着需要启动 cups 的服务,如果需要使用 cups 自带的 webui,可以直接启用 cups.service,这样就能在 http://localhost:631 看到对应的配置页面。 systemctl enable cups.service --now 而如果你正在使用一些集成度较高的 DE 如 KDE 或 GNOME,可以安装 DE 对应的打印机管理程序。在 Arch Linux 下,KDE 自带的打印机管理程序包名为 print-manager,此外还需要安装安装 system-config-printer 打印机功能支持软件包。这种方案则不需要启动 cups.service,只需要启动 cups.socket 即可。 pacman -S print-manager system-config-printer systemctl enable cups.socket 杂项 在常规的流程中,通常会安装 ghostscript 来适应 Non-PDF 打印机,这台 HP Laser 103w 也不例外。 pacman -S ghostscript 如果是 PostScript 打印机可能还需要安装 gsfonts 包,但我这里不需要。 安装驱动 OpenPrinting 维护的 foomatic 为很大一部分打印机提供的驱动文件,Gutenprint 维护的 gutenprint 包也包含了佳能(Canon)、爱普生(Epson)、利盟(Lexmark)、索尼(Sony)、奥林巴斯(Olympus) 以及 PCL 打印机的驱动程序。如果你的打印机型号和我的不同,可以尝试安装这些组织维护的驱动。具体的安装方法同样可以在 ArchWiki 的 CUPS 页面找到。我上一台打印机 HP LaserJet 1020 所需的驱动是在 AUR/foo2zjs-nightly 中取得的。 但 HP Laser 103w 的驱动程序都不在这些软件包中,在 HP 的官网我们可以找到这个页面,包含了 HP Laser 103w 的 Linux 驱动下载地址(已在 web.archive.org 存档)。通过下载下来的文件名,我们可以看见名字为 uld-hp,理论上可以直接通过压缩包内的安装脚本进行安装,但我通过这个名字顺藤摸瓜,找到了 AUR/hpuld 可以直接进行安装。 yay -S hpuld 添加打印机 打开设置中的打印机设置后,选择添加打印机,CUPS 直接帮我们找到了局域网下的打印机,并自动开始搜索驱动程序(虽然没搜到)。 但如果没能自动检测到打印机,也可以使用手动选项中的 AppSocket/HP JetDirect 手动输入打印机的 ip 地址进行配置。 紧接着就到了选择驱动程序的阶段,厂商选择 HP,能够找到「HP Laser 10x Series」的选项,直接选择。 接着就可以完成打印机的添加。 随后便能正常打印文件啦!

2024/7/14
articleCard.readMore

使用动态公网 ip + ddns 实现 rustdesk 的 ip 直连

最近跟风整了一台 n100 的迷你主机装了个 Archlinux 当 HomeServer,搭配上了显卡欺骗器,平常一直远程使用,因此需要实现稳定的远程桌面连接。开源软件 Rustdesk 本身对 Linux 的适配尚可,可惜官方提供的服务器位于境外,且前一阵子因为诈骗相关的风波使得官方对连接做出了一些限制,应当使用自建服务器或者 ip 直连。 单从网络安全的角度出发,最佳实践应该是通过 wireguard 或者别的协议先接入局域网,然后使用局域网内的 ip 直连,这是最稳妥的,但我有点懒,而且我可能会在多个设备上都有控制 HomeServer 的需求,给所有设备配置 wireguard 是一件挺麻烦的事情,因此我决定放弃安全性,直接公网裸奔。 在学校宿舍的电信宽带提供了一个动态公网 ip,因此只需要设置好 ddns 和端口转发就可以拿到一个固定的 domain + port 提供给 rustdesk 直连。 在被控端 Rustdesk 允许直连访问 在「设置」中的「安全」一栏选择「解锁安全设置」,拉到最下面的「安全」栏,勾选「允许 IP 直接访问」,并选择一个端口,范围在 1000 ~35535 之间且不要被本地的其他程序占用,Rustdesk 的默认值为 21118。 可以直接在局域网内的另一台设备进行测试,直接在 Rustdesk 中输入被控端的局域网 ip 和刚刚设置的端口,看看能不能访问得通,如果不行可能需要排查一下被控端访问墙设置的问题。 ddns 由于我的域名是交给 cloudflare 进行解析的,就找了个支持 cloudflare 的 ddns 脚本,大致的部署过程可以参考 「自建基于Cloudflare的DDNS」,不过我小改了一下脚本中获取公网 ipv4 的方式,直接 ssh 到路由器上获取当前的 ipv4 地址,不依赖外部的服务。 WAN_IP=`ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa root@192.168.1.1 'ip -br a' | grep pppoe-wan | awk '{print $3}'` 理论上来说,有不少路由器自身就支持不少域名解析商 端口转发 端口转发需要在路由器的后台设置进行,我这里路由器使用的是 openwrt 系统,大部分路由器应该都支持这个操作。 在「网络」-「防火墙」 选择「端口转发」 新建端口转发,共享名随便填,外部端口是你最终要在主控端输入的端口,内部 IP 地址是被控机 的 IP 地址,可以用 ip -br a 命令看到,内部端口就是上文在 Rustdesk 指定的端口号。 效果 可以直接在主控端口输入 ddns 的域名和端口号,实现远程控制

2024/6/30
articleCard.readMore

使用 Windows 虚拟机运行虚拟专用网客户端为 Linux 提供内网环境

起因 最近在某家公司实习,公司内部的 git 部署在内网环境上,需要通过虚拟专用网的客户端(天翼云的 AONE)才能够正常访问。很可惜,客户端只提供了 Windows 和 MacOS 的版本。 工作的代码总是要提交的,我也不想改变我的开发环境,又不希望在 Windows 上使用 git-for-windows 这个近乎简陋的工具进行代码提交,更别说还有一些别的内网服务接下来可能也会用到。所以最好的办法就是在 Linux 下也配置好能够访问内网的环境。 理论 在 Windows 下使用 AONE 的网络拓扑是这样的 而我的方案则是使用 Windows 虚拟机开启 AONE,并在这台虚拟机上开一个 socks5 server 负责代理 Linux 宿主机需要打到内网服务的流量。网络拓扑如下 根据 bilibili 上技术蛋老师的视频总结,我们应该选择使用网卡桥接的网络配置,只有这个配置方式同时支持「宿主->虚拟机」和 「虚拟机->互联网」的网络。 实操 在 Windows 虚拟机中开启虚拟专用网客户端 开启 AONE,不做赘述 开启 socks server,监听地址为 0.0.0.0 (或者设置为宿主机的 IP 地址) 在「熊孩子(BearChild)」的推荐下,我这里采用的是大名鼎鼎的二级射线(某 V 字开头的常见软件),直接从 GIthub Release 中下载 Windows X64 的压缩包,简单配置下即可,如果没有什么特殊需求的话可以只修改图中的两处配置。 在终端中通过该软件的 run 命令即可开启服务 在宿主机进行测试 我这里使用的是 mzz2017 编写的 gg 命令进行代理,代理服务器的 ip 地址使用虚拟机下 ipconfig 命令获得的 ip 地址,端口号则对应上面配置文件中的 port 参数。 这里 curl 百度得到了正确的相应,说明通道是通的,gg 也可以用于代理浏览器。经实测能够正常访问公司内网服务,不便在博客中展示。

2024/5/23
articleCard.readMore

以 Archlinux 中 makepkg 的方式打开 rpmbuild

在 Redhat 系的发行版上打包软件的时候,会发现与 Archlinux 完全不同的思路。 Fedora 所代表的 Redhat 阵营一看就是那种宏大叙事的大型发行版,rpmbuild 在默认情况下会在 $HOME/rpmbuild 下的一系列文件夹进行构建过程。使用 rpmdev-setuptree 命令会创建好下面这些目录进行构建。 $ tree rpmbuild rpmbuild ├── BUILD ├── BUILDROOT ├── RPMS ├── SOURCES ├── SPECS └── SRPMS Fedora 将所有的软件的构建都集中在一个 rpmbuild 目录中,BUILD 是编译时使用的,BUILDROOT 是最终安装目录,RPMS 是存放最终产物的,SOURCES 是存放源码等文件的,SPECS 是存放指导构建过程的 spec 文件的,而 SRPMS 是 RH 系为了 reproducibility 而单独将 spec 和源文件打包的产物。除了 rpmbuild 命令以外,Fedora 还有一套使用容器构建 rpm 包的 mock 构建系统,与 Archlinux 的 devtools 类似,这里不作过多叙述。 反观 Arch 的构建目录,就有一股浓浓的小作坊气味。每个软件包自己拥有一个目录,指导构建过程的 PKGBUILD 文件、源文件和最终的产物都放在这个目录下,目录下的 src 和 pkg 文件夹分别对应 rpm 的 BUILD 和 BUILDROOT,前者是源文件被解压的目录和编译过程进行的目录,后者是软件最终的安装目录。 $ tree repo repo ├── src ├── pkg └── PKGBUILD 好巧不巧,我偏偏习惯这个小作坊气息的 arch build system,每个软件包独享一个自己的目录,干净又卫生。我自然也希望在 Fedora 下打 rpm 包的时候能够使用类似 Archlinux 下 makepkg 使用的目录结构。 简单了解 在了解一系列 rpmbuild 中宏(macros)相关的知识后,我意识到这并非不可能。 使用如下的命令可以获取目前系统中定义的所有宏 rpm --showrc 而可以使用如下命令检查某一个宏目前被定义成了什么值 rpm --eval "%{_topdir}" 更多关于宏的描述可以在 https://rpm-software-management.github.io/rpm/manual/macros.html 获取 修改路径 我们可以把定义成 $HOME/rpmbuild 的 %_topdir 重新定义成当前目录。 在 $HOME/.rpmmacros 中,去除顶部对 %_topdir 的定义,重新填上以下这些定义,即可初步完成我想要的效果。 %_topdir %(pwd) %_builddir %{_topdir}/src %_buildrootdir %{_topdir}/pkg %_rpmdir %{_topdir} %_sourcedir %{_topdir} %_specdir %{_topdir} %_srcrpmdir %{_topdir} 现在在任何一个目录下执行 rpmbuild 相关命令,都会把 src 认为是构建目录,pkg 是最后安装目录,spec 文件和源文件早当前文件夹下,构建产物在当前文件夹下的 x86_64(或者别的架构名,这一层目录我还没有找到应该如何去掉)下。 自动安装依赖文件 Fedora 中的 rpmbuild 不带有 makepkg -s 的功能,不能自动安装依赖。不过这也不意味着需要自己傻傻地去翻 spec 看看需要哪些构建依赖。可以使用 dnf 的 builddep 命令实现 sudo dnf builddep ./*.spec 不过 dnf 没有什么完成构建后自动卸载依赖的选项。这些依赖装完以后就一辈子赖在你的电脑上了,才不是,可以在构建完成后使用 dnf 自带的后悔药功能撤销上一条命令执行的效果。 sudo dnf history undo 0 不过如果在 builddep 过程中,dnf 从 updates 源里更新了一些软件,那么它在 undo 时可能就没法获取更新前的软件版本。会有 Cannot find rpm nevra 的提示 可以使用 --skip-broken 命令跳过那些没法找到老版本的软件,继续卸载其余的软件。 自动下载源文件 很多使用 spec 中会在 source 里写上下载地址,而不是附上源码文件。rpm 似乎因为一些原因禁止了 rpmbuild 自动下载源文件的功能。可以通过在使用 rpmbuild 的时候带上 --undefine=_disable_source_fetch 取消定义这个行为,或者干脆在调用 rpmbuild 之前执行一遍 spectool -gR *.spec 这样也能自动下载源文件。 构建行为 makepkg 的默认构建行为就是只构建最终的安装包,Archlinux 中并没有 Fedora 那样打 source rpm 保证 reproduceability 的行为,这在 rpmbuild 中对应的是 -bb 选项。 使用 rpmbuild -bb *.spec 即可 上面介绍完了 rpmbuild 和 makepkg 的主要差异,应该可以自己搓一个 rpmbuild-wrapper 去实现以 makepkg 的方式打开 rpmbuild 的目标了,具体的 wrapper 脚本我就不放出来献丑了。

2024/5/3
articleCard.readMore

使用 Github Action 更新用于 rpm 打包的 spec 文件

有一些软件包的上游本身就是使用 Github Action 发版的,每次 commit 都会触发 Github Action 去构建并分发新版本,使用构建时的时间日期作为版本号。针对这种包,手动更新费时费力,而规范的 specfile 应当是更新 %changelog 的,因此应当是使用 rpmdev-bumpspec 命令。只不过 rpmdev-bumpspec 需要在 rpm 系发行版或者装有 rpm 系列依赖包的发行版下执行,这不是随随便便一个 Linux 环境就能运行的。 我找到了 netoarmando/rpmdev-bumpspec-action 这个 Github Action,它通过启动一个 Fedora 的 docker 实现了使用 rpmdev-bumpspec 的效果。虽然 release 中只有一个 2021 年构建的 v1 版本,~~但 Fedora 的版本高低不影响 rpmdev-bumpspec 的效果。~~但每次 Github Action 执行时都会使用 fedora:latest 的 docker 重新构建一遍,不用担心 fedora 版本过低。 于是我们便解决了最核心的问题——处理 spec 文件。接下来只要补充好头尾的步骤即可。 首先使用 actions/checkout 释出仓库内的文件 - name: Checkout uses: actions/checkout@v2 with: fetch-depth: 0 通过 shell 命令获取仓库内 spec 文件的版本号,存入 $GITHUB_ENV - name: Get Current Version run: | CURRENT_VERSION=`grep -E '^Version:' *.spec | awk '{print $2}'` echo "CURRENT_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV 通过 Github API 获取目标软件的最新版本号,存入 $GITHUB_ENV - name: Export latest geoip version run: | NEW_VERSION=`curl -s https://api.github.com/repos/{user_name}/{repo_name}/releases/latest | jq -r '.tag_name' | sed 's/v//g'` echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV 当仓库内 spec 版本号与软件最新版本号不一致时,运行 rpmdev-bumpspec - name: Run rpmdev-bumpspec action if: ${{ env.CURRENT_VERSION != env.NEW_VERSION }} uses: netoarmando/rpmdev-bumpspec-action@v1 with: specfile: '{filename}' new: ${{ env.NEW_VERSION }} userstring: "username <username@mail.com>" 当仓库内 spec 版本号与软件最新版本号不一致时,保存更改,推入仓库。 - name: Commit changes if: ${{ env.CURRENT_VERSION != env.NEW_VERSION }} run: | git config --local user.email "zhullyb@outlook.com" git config --local user.name "zhullyb" git add . git commit -m "upgpkg: v2ray-geoip@${{ env.NEW_VERSION }}" git push (可选)当仓库内 spec 版本号与软件最新版本号不一致时,通过 curl 语句触发 copr 的 webhook,让 copr 进行构建。 - name: trigger copr webhook if: ${{ env.CURRENT_VERSION != env.NEW_VERSION }} run: | curl -X POST ${{ secrets.COPR_HOOK_URL }}v2ray-geoip/ 最终的 yml 文件可以参考这里

2024/4/29
articleCard.readMore

使用 Python 生成甘特图(Gantt Chart)

在写操作系统的作业的时候有几道题给出了几个进程的相关信息,要求我们画出几种简单调度的甘特图。操作系统的作业一直是电子版,上传 pdf 即可的。我觉得手画甘特图拍照嵌入 pdf 中不太优雅,过于掉价,因此就想直接生成甘特图嵌入。 在谷歌搜寻了一番,我发现现在的甘特图生成网站都太现代化了,根本不是操作系统课上教的样子了。 所幸我找到了 gao-keyong/matplotlib-gantt,虽然只有两个 star(没事,加上我就 3 stars 了),但确实能用,README 中的样例也是我期望的样子。 项目中自带了一个 jupyter 的示例,算得上是非常简单易上手的了,依赖方面只要装好 matplotlib 就可以使用,不存在依赖地狱。尽管是三年前的项目,在我本机的 Python 3.11 上仍然能够正常运行。 tuple 中的第一个数字表示从当前时间开始,第二个数字表示持续时间。每一个表示 category 的 list 中可以存在多个 tuple。 给一些咱生成的例子。 from gantt import * category_names = ['P1', 'P2', 'P3', 'P4', 'P5'] results = { 'FCFS': [[(0,2)], [(2,1)], [(3,8)], [(11,4)], [(15,5)]], 'SJF': [[(1,2)], [(0,1)], [(12,8)], [(3,4)], [(7,5)]], 'non-compreemptive priority': [[(13,2)],[(19,1)],[(0,8)],[(15,4)],[(8,5)]], 'RR (quantum=2)': [[(0,2)], [(2,1)],[(3,2),(9,2),(15,2),(18,2)], [(5,2),(11,2)], [(7,2),(13,2),(17,1)]] } arrival_t = [0, 0, 0, 0] gantt(category_names, results, arrival_t).show() from gantt import * category_names = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6'] results = { '': [[(0,20)], [(25,10),(45,10),(75,5)], [(35,10),(55,5),(80,10)], [(60,15)], [(100,5),(115,5)],[(105,10)]], } arrival_t = [0] gantt(category_names, results, arrival_t).show()

2024/4/24
articleCard.readMore

uniapp 中的图片预加载

最近在做微信小程序的时候遇到了图片资源过大无法正常打包的问题,没什么太好的方法,只能是使用图床托管这些图片资源。但部分图片的体积实在太大,即使是采用了境内 cdn 的图床,即使是采用 webp 对图片进行了压缩,部分图片都需要小几秒去把图片加载出来,这导致的用户体验就不是很好了,因此我们需要实现图片预加载的功能。 在 uniapp 的官方文档中,我找到了 uni.preloadPage(OBJECT) 方法。很可惜,这个方法并不支持微信小程序,自然不能完成被预加载页面的图片资源预加载。 经过搜索,在一篇奇奇怪怪的文章中提到: 在UniApp中,图片预加载可以通过使用uni.getImageInfo方法来实现。这个方法可以获取图片的信息,包括宽度、高度等。可以在应用启动时就开始加载图片,以提高后续图片显示的速度。 很遗憾,经过实测,提前使用 getImageInfo() 方法并不能实现图片的预加载。getImageInfo() 获取时的 Type 是 xhr,而后续图片加载时的 Type 为 webp,图片会被重复下载,并没有实现预加载的作用。 上图中,蓝色部分是 getImageInfo() 的网络请求,红色部分是真正的图片加载请求,可谓是一点用都没有,该加载慢还是加载慢。 那有没有什么办法能够实现预加载呢?我没找到优雅的方法,选择在应用的首页创建一个 display: none 的 view 将所有的图片先加载一遍。 <template> <view style="display: none;"> <image v-for="image in imageToPreload" :src="image" /> </view> </template> <script setup lang="ts"> const imageToPreload = [ "https://http.cat/100", "https://http.cat/200", "https://http.cat/300", "https://http.cat/400", "https://http.cat/500" ] </script> 可以看到,红色部分的资源在 size 那一栏变成了 (disk cache),加载时间也明显降低,虽然方法不优雅,但起码实现了图片资源的预加载。

2024/4/1
articleCard.readMore

小记 - 尝试拼凑出 apt 仓库中的 deb 包下载地址

大概一周前,有一个来源不明的 Linux 微信,从包的结构来看是基于 qt 实现的图形化界面,deb 包中的 control 信息表明是腾讯团队官方出品的。今天听人说 UOS 的商店上架了最新的微信,便尝试从 UOS 的官方仓库提取下载链接,帮助 AUR Maintainer 获取到新的地址。 在我的《deepin-elf-verify究竟是何物?》这篇文章中,我成功从 uos.deepin.cn 下载到了来自 UOS 中的软件包。可惜,当我采用同样的方法搜索 weixin 或者 wechat 字样时,没有得到任何结果。 UOS 上的软件来源起码来自两个仓库,一个是与系统有关的软件,比如 Linux Kernel,GCC 一类开源软件,应该就是来自我之前下载到 deepin-elf-verify 的那个源。除此之外,还有一个 appstore 源,里面存放的都是应用商店中上架的软件(大部分可能是闭源的)。 在 chinauos.com 下载到最新的 ISO 安装镜像后,直接在虚拟机中走完正常的安装流畅,然后直捣黄龙。 可以看出,/etc/apt/sources.list.d/appstore.list 文件中列出的源很有可能就是我们要找的新版微信的所在源。 可惜直接访问的时候,源地址给出了 403。他们似乎不愿意公开源地址的 filelist index。 不过没关系,既然 UOS Desktop 目前仍然依赖 APT 实现软件安装,那它的源应该仍然符合 Debian 的 APT Repository 目录结构。 根据 DebianWiki 中的描述 gives an example: deb https://deb.debian.org/debian stable main contrib non-free An archive can have either source packages or binary packages or both but they have to be specified separately to apt. The uri, in this case https://deb.debian.org/debian specifies the root of the archive. Often Debian archives are in the debian/ directory on the server but can be anywhere else (many mirrors for example have it in a pub/linux/debian directory, for example). The distribution part (stable in this case) specifies a subdirectory in $ARCHIVE_ROOT/dists. It can contain additional slashes to specify subdirectories nested deeper, eg. stable/updates. distribution typically corresponds to Suite or Codename specified in the Release files. FIXME is this enforced anyhow? To download packages from a repository apt would download an InRelease or Release file from the $ARCHIVE_ROOT/dists/$DISTRIBUTION directory. 我尝试了访问 https://pro-store-packages.uniontech.com/appstore/dists/eagle-pro/Release,获得了一系列索引文件的索引。 第一段中就能看到熟悉的 Packages 文件。根据我 deepin-elf-verify 相关博客中记载,这个文件中会保存 deb 文件的相对路径。我们先拼出 amd64 架构的 Packages 文件下载链接: https://pro-store-packages.uniontech.com/appstore/dists/eagle-pro/appstore/binary-amd64/Packages 这里可以看到源中每一个 deb 包的信息。图中红色方框框出的便是其中一个 deb 包在源中的相对路径。 我们可以使用 grep 命令去检索 weixin 或者 wechat 关键词 curl -sL https://pro-store-packages.uniontech.com/appstore/dists/eagle-pro/appstore/binary-amd64/Packages | grep -E "weixin|wechat" 在这个路径前加上之前 appstore.list 文件中给出的 url 前缀,即可拼凑出 deb 包的完整下载地址: https://pro-store-packages.uniontech.com/appstore/pool/appstore/c/com.tencent.wechat/com.tencent.wechat_1.0.0.236_amd64.deb 放到浏览器中尝试,果然可以正常下载

2024/3/13
articleCard.readMore

在 Linux 下使用 mitmproxy 抓取 HTTPS 流量

作为部分 AUR Package 的 maintainer,一直以来我都有在 Linux 下抓取 https 流量的需求,比如抓取应用内的更新检测时访问的 url 地址。之前一直没有空去研究,趁着最近课少,总算是完成了这个目标。 在这里我使用的 mitmproxy,基于 python 和 webui 的一款开源简洁的流量代理软件,可以用于抓取 https 流量信息。 安装 mitmproxy 在 Arch Linux 下,官方 extra 源中已经打包好了这款软件,直接使用下面的命令即可完成安装。 sudo pacman -S mitmproxy 尝试运行 mitmweb 安装完成后,我们将会获得三个新的命令可用: mitmdump mitmproxy mitmweb 我们只要使用 mitmweb 即可同时打开 8080 的代理端口和 8081 端口的 webui。访问 http://127.0.0.1:8081 即可看到 mitmproxy 的网页。 当然,也可以在 mitmweb 命令后面追加 -p 和 --web-port= 分别设置代理端口和 webui 的端口。 首先,我们先运行一次 mitmweb 安装 ca 证书 为了解密 https 流量,我们需要为系统安装上 mitmproxy 自己的证书文件,让系统信任我们的证书。 先来看看 /usr/share/ca-certificates/trust-source/README 这个文件 This directory /usr/share/ca-certificates/trust-source/ contains CA certificates and trust settings in the PEM file format. The trust settings found here will be interpreted with a low priority - lower than the ones found in /etc/ca-certificates/trust-source/ . ============================================================================= QUICK HELP: To add a certificate in the simple PEM or DER file formats to the list of CAs trusted on the system: Copy it to the /usr/share/ca-certificates/trust-source/anchors/ subdirectory, and run the update-ca-trust command. If your certificate is in the extended BEGIN TRUSTED file format, then place it into the main trust-source/ directory instead. ============================================================================= Please refer to the update-ca-trust(8) manual page for additional information. 这份文件告诉我们可以在 /usr/share/ca-certificates/trust-source/anchors/ 路径下放置 PEM 证书文件,并使用 update-ca-trust 命令更新系统的信任。 mitmproxy 软件第一次运行时,将会在当前用户的 $HOME/.mitmproxy/ 文件夹下生成证书,我们打开这个文件夹,发现一共有六个文件: mitmproxy-ca-cert.cer mitmproxy-ca-cert.p12 mitmproxy-ca-cert.pem mitmproxy-ca.p12 mitmproxy-ca.pem mitmproxy-dhparam.pem 我们这里需要将 mitmproxy-ca-cert.pem 文件复制到 /usr/share/ca-certificates/trust-source/anchors/ 路径下 sudo cp $HOME/.mitmproxy/mitmproxy-ca-cert.pem /usr/share/ca-certificates/trust-source/anchors/ 随后执行 update-ca-trust sudo update-ca-trust 这样便完成了 ca 证书的安装 使目标软件使用 8080 端口通信 其实我试过使用透明代理进行抓包,只不过我的 Archlinux 是作为日常主力机使用的,系统无时无刻不在向外通信,透明代理以后 mitmproxy 的 webui 各种刷屏,便放弃了这个想法,选择指定目标软件使用 8080 端口通信。 网上比较常见的做法是使用 proxychains-ng 代理目标软件。这个方案是可行的,只不过我这边测试下来,部分软件使用 proxychains 代理以后出现了仍然不使用代理、无法联网、甚至直接崩溃的情况。 因此我转向了 gg。gg 和 proxychains-ng 的定位相同,都是使目标命令通过指定的代理进行通信,只不过 gg 解决了部分 golang 编写的软件无法被 proxychains 代理的问题,并支持一些常见的用来国际联网的协议。 在不对 gg 进行配置的情况下,每次启动时,gg 都会要求我们输入代理地址,这正合我意。 此时,软件正常启动,流量全部经过 mitmproxy,可以在 webui 上看到具体情况 抓包成功 我们可以看到 mitmproxy 成功捕获并解密的 https 流量,针对图片等信息甚至可以直接实现预览。

2024/2/29
articleCard.readMore

如何使用 docker 部署 onemanager

部署方法 如果你只是想找一个 OneManager-php 的 Docker 部署方法,直接看 https://github.com/zhullyb/OneManager-php-docker 一直以来,我都是 OneManager-php 的忠实用户。这些年来,尽管有 alist 这种 UI 好看,多种网盘高度聚合的项目逐渐取代了 onemanager 的生态位,但 onemanager 支持文件分片上传、上传流量不经服务器的特点还是让我非常满意。前一阵子,glitch 暂停了针对项目自定义域名的支持,因此在我手贱地取消了项目原本绑定的域名后,迫切地需要寻找一个新的部署的平台,只不过 onemanager 项目现在列出的方案都不太让我满意,因此我就萌生出了在 vps 上自己部署的想法。 Docker 镜像选用 vps 上自己部署 php 项目,最简单的方法是使用 Docker,使用 Docker 就可以免去配置 nginx 或者同类产品的 php-fpm 配置才怪。我打开 Docker 提供的 php 官方镜像,最小的镜像是带-cli后缀的,这个镜像就不适合进行部署,php 内置的开发服务器是单线程的,当同时打开两个网页访问开发服务器的时候,其中一个网页就会卡住;以-fpm结尾的镜像变体很明显,仍然需要去 nginx 或同类产品的配置文件那边去配置 fpm,这给部署了好几次 php 项目的我带来的心理阴影;剩下一个就是-apache后缀、使用 apache server 提供 php 服务的镜像,体积虽然大了点,但好在操作简单,只需要将 php 文件放进 /var/www/html,启用 php 的相关拓展,启用 apache 的相关功能即可。 php 拓展 php 的拓展可以使用镜像自带的 docker-php-ext-install 和 docker-php-ext-enable 命令进行操作,此外还有一个 docker-php-ext-configure 命令可以配置相关的拓展,不过我并不是 php 开发者,不熟悉拓展有什么好配置的。 OneManager-php 没有依赖任何的 php 拓展,因此这个步骤可以直接跳过。 Apache Server 配置 和 php 拓展一样,镜像内也提供了几个命令进行 Apache Server 的配置,分别为 a2disconf、a2dismod、a2dissite、a2enconf、a2enmod、a2ensite、a2ensite。 OneManager-php 在部署的时候依赖于 Apache Server 的 rewrite 的模块,因此在 Dockerfile 中需要使用 a2enmod rewrite 开启 rewrite 支持。至于别的 Apache Server 配置,都可以通过项目中的 .htaccess 文件进行配置。 .htaccess 文件纠错 在 OneManager-php 仓库中,.htaccess 文件有一些小问题。 RewriteRule ^(.*) index.php?/$1 [L] 这行配置原本是将访问的路径追加到 index.php?/ 后面的意思,但 一旦路径中出现了 [、] 或者空格等字符时,会触发 Apache 自带的保护,因此我们将这行改成下面这个样子即可。 RewriteRule ^(.*) index.php [QSA,L] 原项目合并了我的 PR,因此这一过程不再需要。 处理文件权限问题 OneManager-php 在运行过程中,会有针对配置文件的读写操作,此外还内置了一键更新的功能,因此会对路径内的文件进行读写,我们需要确保 php 在运行过程中有权限对这些文件进行读写。 可以直接将 /var/www/html 路径的所有权转给 www-data 用户。 chown -R www-data:www-data /var/www/html 最终的 Dockerfile FROM php:8-apache RUN a2enmod rewrite COPY OneManager-php /var/www/html RUN chown -R www-data:www-data /var/www/html 其实一共就 4 行,还是挺简单的。

2024/2/11
articleCard.readMore

crontab 中简单的@语法糖

说来惭愧,其实我用了这么久的 Linux,一直没有学会编写 crontab 脚本。一行的开头写上五位莫名其妙的数字或星号,后面跟上需要执行的命令,看上去很 kiss,但我确实记不住,以至于我现在每次写 crontab 都是让 ChatGPT 来帮我写。 不过我最近查阅 Linux 下设置开机自启脚本的方案的时候,意外地看到 crontab 中居然可以用 @reboot command 的方式去写,这让我意识到 crontab 也是有一些简单的语法糖的。在查阅了 crontab 的 manual 后,我发现一共有下面这么几种 @ 写法的语法糖。这是在全网大部分的 crontab 中文教程中是没有的。 语法糖 执行条件 等效表达式 @reboot 开机时候运行 @yearly 一年一次 0 0 1 1 * @annually 一年一次 0 0 1 1 * @monthly 一月一次 0 0 1 * * @weekly 一周一次 0 0 * * 0 @daily 一天一次 0 0 * * * @hourly 一小时一次 0 * * * * 这几个简单的语法糖可以满足大部分 crontab 的情况,免去了对使用者学习并记忆 crontab 的表达式的要求。 比如说,如果我希望我的系统在每次开机时都用 TG Bot 发送一条上线信息,那就是 @reboot curl -s -X POST https://api.telegram.org/bot{id}:{apikey}/sendMessage -d chat_id={uid} -d text="`date`"

2024/2/8
articleCard.readMore

备份 umami 数据库,并使用 TG Bot 保存 dump 文件

前一阵子看到点墨的博客「定时备份mysql/mariadb数据库并上传至tgbot」,我意识到个人站点的数据库 dump 使用 TG Bot 存放是一个非常合适的做法。个人站点的数据库体积本身就不大,TG Bot 又有官方提供的 api,非常适合自动化任务。我就寻思着给我的 umami 数据库也写个定时任务备份一下,也不至于之前做一次迁移数据全部爆炸的悲剧重演。 我的 umami 是「使用 vercel+supabase 免费部署 umami」部署出来的,数据库在 supabase 上,因此我们先打开 supabase 的 dashboard,获取到数据库的 url。 密码我自然是不记得了,不过好在 Firefox 的密码管理器帮我记住了,直接去设置里就能找到。即使密码忘了也不要紧,往下翻有重置密码的按钮。 随后就要开始编写我们的教本了,这是我的 #!/bin/bash DATABASE_URL="postgres://" DATE=$(date '+%F') TG_BOT_TOKEN='1145141919:ABCDEFGHIJKLVMNOPQRSTUVWXYZabcdefgh' TG_CHAT_ID='9191415411' pg_dump ${DATABASE_URL} > umami_dump_${DATE}.sql curl -F document=@umami_dump_${DATE}.sql https://api.telegram.org/bot${TG_BOT_TOKEN}/sendDocument?chat_id=${TG_CHAT_ID} rm umami_dump_${DATE}.sql 将这段代码保存为 umami_db_dumper.sh,随后 chmod +x ./umami_db_dumper.sh 授予可执行权限。 可以先在命令行中执行命令试一下这段脚本是否正常工作 ./umami_db_dumper.sh 这段代码在我本机正常工作,可惜在我的 Ubuntu VPS 上报错 pg_dump: error: server version: 14.1; pg_dump version: 12.17 (Ubuntu 12.17-0ubuntu0.20.04.1) pg_dump: error: aborting because of server version mismatch 看上去是 VPS 上的 PostgreSQL 版本过低,Google 搜索一顿后,我在一篇「Upgrade pg_dump version in ubuntu | by Anushareddy」 文章中找到了方案,添加 PostgreSQL 官方提供的 apt 源将 VPS 上的 PostgreSQL 更新到新版即可解决。 wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - echo "deb http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list apt update apt install postgresql-client 确保脚本正常工作后,使用 crontab -e 设置自动任务 0 2 * * * /root/umami_db_dumper.sh

2024/2/1
articleCard.readMore

在 JavaScript 中,箭头函数中的 this 指针到底指向哪里?

这学期期末复习的时候,学校里负责上 JavaScript 的老师给我们提出了一个问题。下面这段代码中,a.u2() 在 ES Module 下执行会抛出 TypeError 的异常,在 CommonJS 下运行则会输出 undefined,而 B 这个类的 u2 函数则能够在对象实例化以后正常运行。 const a = { x: 3, u1: function () { console.log(this.x) }, u2: () => { console.log(this.x) } } class b { x = 3 u1 = function () { console.log(this.x) } u2 = () => { console.log(this.x) } } a.u1() // 3 a.u2() // undefined new b().u1() // 3 new b().u2() // 3 这个问题涉及到 JavaScript 中箭头函数的作用域以及 this 指向。 **在 JS 中使用 function 关键字定义的普通函数中,this 指针遵循一个规则:谁调用指向谁。**即 obj.func() 这种调用情况下,func 方法内部的this指向obj;如果没有调用者,则严格模式下 this 为 undefined,非严格模式下 this 指向window(浏览器)或者global(node环境)。 而箭头函数比较特殊,箭头函数的 this 在定义时就被绑定,绑定的是定义时所在作用域中的 this。 在老师给的示例代码中,第一行定义了 a 这个对象字面量,而定义对象字面量不会创建新的作用域,因此 a 中定义的 u2 的 this 指向的是全局对象。因此在 Es Module 默认启用 strict mode 的情况下,全局对象的 this 指向 undefined,进而导致 a 的 u2 内 this 也指向 undefined,this.x 就抛了 TypeError;而在 CommonJS 未启用 strict mode 的情况下,全局对象的 this 指向全局对象,因而 u2 内的 this 也指向全局对象,因此 this 存在,this.x 就不会抛 TypeError,只会报 undefined。 而 B 类在对象初始化阶段拥有一个新的作用域,因此箭头函数的 this 能够正确指向 B 被实例化出来的对象,因此也就能够正确读取到 this.x 的值。 理论上来说,我们可以给全局对象也赋一个不一样的 x 值,这样 a.u2() 就能够读取到全局对象中的 x 值,验证我们的结论。 在浏览器中,可以在代码的头部加一行 var x = 10 或者 window.x = 10,可以看到a.u2() 顺利的输出了 10,验证了我的结论。 但在 Node.js 中,直接使用 var x = 10 或者 global.x = 10 并不能达到我们想要的效果。因为Node.js 中的每个 CommonJS 模块都有其自己的模块作用域,即模块的顶层作用域不是全局作用域。在模块内部,this 关键字不是指向 global 对象,而是指向模块的导出对象。这是为了确保模块内部的作用域隔离和模块的封装性。 那么我们可以通过为模块的导出对象添加一个 x 属性来验证我们的结论,我们可以使用 exports.x = 10 来为模块的顶层作用域添加一个值为 10 的 x 属性。 参考文章 箭头函数表达式 - JavaScript | MDN ES6箭头函数作用域的问题 ES6箭头函数的this指向详解

2024/1/14
articleCard.readMore

结合 Vue.js 与 php 完成的 web 期末大作业,讲讲前后端分离站点开发与部署中可能遇到的 CORS 跨域问题

在上一篇博客中,我讲到了 web 期末大作业的上云部署。整个项目是使用 Vue.js 作为前端,php 作为后端,mysql 作为数据库实现的。 在使用 Vue.js 开发前端界面时,我选择了使用 vite 脚手架帮助开发,这意味着我的作品将使用前后端分离的架构实现。因此在开发部署过程中均遇到了跨域的问题,故写下这篇博客记录下解决方案。 基于后端返回对应 http 响应头的解决方案 开发阶段 在我完成前后端的开发,并且经过 Apifox 的 mock 测试后,第一次在浏览器尝试前后端对接,遇到了 CORS Missing Allow Origin 的报错。 vite 启动的 dev 开发服务器使用的域是 http://localhost:5173 ,而 php 后端我指定的是 http://127.0.0.1:8080 ,前后端并不运行在一个域下,前端使用 Axios(AJAX) 向后端发送请求获取资源输入 CORS 跨域资源共享的范畴。 关于跨域资源共享 CORS 的相关内容,阮一峰老师在 2016 年就已经在他的博客中有过解释,看了下也是全网中文内容中解释得比较通俗易懂的,因此本文在这方面不过多做解释。错误的提示信息是 Missing Allow Origin,结合阮一峰老师的博文,我们应该在后端向前端发送的 http 响应头中添加 Access-Control-Allow-Origin 这一字段。 在一般的前后端分离项目(不涉及 cookie 等 Credentials 属性)中,我们可以将这一字段设置为 * 通配符,默认允许所有的域向自己发起跨域资源请求。php 可以通过下面这行代码很方便地进行设置: header('Access-Control-Allow-Origin: *'); 但在用户的注册登录方面,我使用了 session 作为用户的登录凭据。阮一峰老师关于 CORS 的博文中有这样一句话: 需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。 因此,我们必须明确指定 Access-Control-Allow-Origin 字段为前端所使用的域,写上 http://localhost:5173 才行。 header('Access-Control-Allow-Origin: http://localhost:5173'); 再次刷新网页,获得了新的错误 CORS Missing Allow Credentials 这个问题处理起来也简单 header('Access-Control-Allow-Credentials: true'); 再次运行网页,跨域问题成功解决。 部署阶段 顺着这个思路进行下去,我们在部署阶段解决跨域问题需要做的事情很简单。提前将前端部署起来,将前端的域写到后端返回给前端的 http 相应头中即可。需要注意的是,Access-Control-Allow-Origin 字段仅允许填写一个值,如果需要同时允许来自多个不同域的跨域资源共享,后端部分需要根据前端发来的请求头中的 Origin 字段相应地设置响应头中的 Access-Control-Allow-Origin 。当然,nginx 等先进的 static server 也支持劫持 http 请求,添加相关的 Access-Control 语句,也可以在这一层解决这个问题。 直接规避跨域的方案 上面通过后端返回带有 Access-Control 语句相应头的解决方案确实可以解决问题,却显得不够优雅。开发和部署阶段都要手动的去指定前端的域来允许跨域资源共享,这一点过于麻烦了,因此引出了下面的解决方案。 开发阶段 在 vite(或者其他同类开发服务器)的帮助下,我们可以使用前端的开发服务器去反向代理后端服务,也就是让前端的请求打到前端服务器上,由前端服务器去返回后端服务器返回的结果。 在 vite.config.ts 配置文件下,我将原本的 export default defineConfig({ plugins: [vue()], }) 换成了 export default () => { process.env = { ...process.env, ...loadEnv(process.cwd(),'') }; const config = { plugins: [vue()], server: { proxy: { '/api': { target: http://127.0.0.1:8080, changeOrigin: true, secure: false, } } } } return defineConfig(config) }; 同时将 Axios create 时的 baseURL 参数去除。 这样一套组合拳下来,将所有打向 /api* 的请求和响应通过前端的开发服务器作为中介做了中转,让浏览器以为并没有跨域(事实上也没有跨域),从而解决了相关的问题。 部署阶段 在开发阶段,我们通过 vite 的开发服务器做反向代理规避了跨域请求,但在部署阶段就用不了了。由于 vite 服务器的性能太弱,一般情况下我们是不会在生产环境中使用 vite 作为正式的服务器的,而是使用 vite build 出网站的静态网页资源,通过 nginx 等 static server 去向用户提供前端网页。而通过 vite build 出来的静态网页资源本身是不具备反向代理的能力的,这意味着没法在前端侧规避跨域问题。此时,我们应该配置 nginx 规避跨域问题。我一向不怎么使用 nginx,使用的是它的平替品 caddy,因此 nginx 的配置文件需要大家自行搜索,我的 caddyfile 在上一篇博客中已经给出,仅供参考。

2024/1/10
articleCard.readMore

vuejs、php、caddy 与 docker —— web 期末大作业上云部署

这学期修了一门叫《用HTML5 和 PHP编写JavaScript,jQuery 和 AJAX脚本》的 web 课(对,听起来很奇怪的名字)。期末大作业是写一个影评系统,前端允许使用框架,后端仅允许使用 php,具体的作业要求如下 (源码会在验收结束以后开源) 大作业写了得要有三个礼拜,工作时长加起来得有 30 个小时,想着验收之前上线一段时间积累一些评论数据,验收的时候也会更加顺利一些,于是就开始尝试在服务器上部署。部署的过程还是比较复杂的,所以写下这篇博客记录一下。 后端部分 早前有《PicUploader使用系列(一)——在Archlinux上使用Caddy部署PicUploader》的经验,便觉得使用 Caddy + php-fpm 部署的方式多少有点麻烦了,这次便尝试了使用 Docker 部署、Caddy 反代的方式。 Dockerfile 如下: FROM php:8-apache RUN docker-php-ext-install mysqli RUN a2enmod rewrite COPY . /var/www/html EXPOSE 80 在后端的根目录下有一个 .htaccess 文件,将所有的请求都交给 index.php 来处理,这样就可以根据我的上一篇博客中所提到的方式去构建不使用任何 php 框架实现的简易 router 效果 RewriteEngine On RewriteRule ^(.*) index.php [QSA,L] 构建 Docker 镜像时使用 docker build . -t mrs-php 命令,运行 docker 容器时使用命令 docker run -d \ -p 7788:80 \ --name mrs-php \ -v /path/to/uploads:/var/www/html/uploads \ --restart unless-stopped \ mrs-php 这样,后端就在 7788 端口上开起来了,后续 Caddy 只要将打到 /api/* 和 /uploads/* 的请求转发到 7788 端口即可,避免了使用 php-fpm 时需要的配置。uploads 目录是用来存放图片的,我将这个路径挂在在宿主机的目录下,方便备份导入等操作。 mysql 连接时的小插曲 需要注意的是,在 Docker 容器中运行的 php 如果想要访问宿主机上的 mysql,需要注意修改 mysql 服务器的 ip 地址,并允许 mysql 接收来自非本机的请求。 在宿主机中运行 ip -br a 命令可以看到 docker 所采用的虚拟网卡的 ip 地址 docker0 UP 172.17.0.1/16 fe80::42:eff:febf:b26c/64 我这边得到的 ip 地址是 172.17.0.1,所以在 php 那边访问的数据库 ip 地址就应该是 172.17.0.1,而非 localhost 或者 127.0.0.1 此外,需要允许宿主机的 mysql 接收来自 Docker 容器的请求 使用 docker network inspect bridge 命令可以查到 docker 容器的 ip 地址,接着需要去允许来自这个 ip 的请求。建议去网上自行搜索,因为 mysql 语句我自己也不熟悉。我使用的 mysql 版本是 8,语句似乎和以前的版本不兼容?我使用下面三个命令轮着输就好了(有时候报错,有时侯又不报错),有大佬懂的话评论区讲讲。 use mysql; GRANT ALL ON *.* TO 'root'@'%'; update user set host='%' where user='root'; GRANT ALL ON *.* TO 'root'@'%'; 前端部分 前端部分部署起来没什么难度 我使用的是 vite 开发的 vuejs 项目,直接使用 pnpm build 构建出静态文件,然后放入了 /var/www/mrs 目录,这部分没什么可说的 Caddy 配置 Caddy 配置如下 example.com { handle /api/* { reverse_proxy localhost:7788 } handle /uploads/* { reverse_proxy localhost:7788 } handle /* { root * /var/www/mrs file_server try_files {path} / } } 将打到 /api/* 和 /uploads/* 都交给 7788 端口的后端进行处理,前端部分要使用 try_files 将请求都指向 / 或 /index.html 交由 vue-router 处理,否则 caddy 就找不到对应的文件了。这里我尝试过使用 route 关键词代替 handle,但 try_files 的功能没有生效,这两者的区别官方文档中有提到,但我没看懂,等我以后看看有没有机会去折腾了。 参考: 使用Caddy配置同一域名下的前后分离 Caddy 2

2023/12/27
articleCard.readMore

【翻译】使用 PHP 构建简单的 REST API

我这学期有一门偏向前端的 WEB 课程,期末大作业要求使用 PHP 作为后端语言实现一个简单的影评系统,应该是不允许使用框架,使用中文关键字在搜索引擎上搜了一阵子似乎没有可供参考的案例,后来就找到了这篇博客,当中的许多观点与我不谋而合,因此我将这篇博客翻译成中文,原文戳这里: https://amirkamizi.com/blog/php-simple-rest-api 介绍 上周 @rapid_api 发了一个非常好的关于使用 nodejs 和 express 创建 REST API 的教程帖子。我想要帮助你使用 PHP 开发同样简单的 REST API。 首先,如果你不了解 REST API,请务必查看这个 Twitter 帖子。 目标 在我们开始之前,我想提一句,当我写这篇帖子的时候,我想确保: 我使用单纯的 PHP,不使用框架 我使用最简单的函数和结构体以便所有人都可以理解并跟上 我将主体部分分开 现在让我们开始吧 准备 在我本地的机器上,我创建了一个叫 api 的文件夹于 xampp > htdocs,在里面有一个叫 index.php 的文件 如果你没有 xampp 或者你不知道如何把 php 跑起来,请务必查看这篇文章 现在,如果你尝试访问 localhost/api,你将得到一个空的响应,因为 index.php 文件是空的 优雅的 URL 项目中,我们需要处理的第一件事是 url REST API 的关键特性之一是每一个 url 负责一个资源和一个操作 问题 这时候如果我创建一个 users.php,我需要访问 localhost/api/users.php 我需要为每一个 user id 创建一个新的文件 localhost/api/users/1.php localhost/api/users/2.php 以此类推。 这种方案有两个问题 为每个用户创建一个新文件是非常无聊和耗时的 路由不优雅,每个路径后面都带有 .php 解决方案 让我们解决这个问题。 正如我所提到的,我不想使用任何框架,并且我想使用最简单的、最让人能够理解的方案 让我们看看如何解决这个问题 在 api 文件夹下创建一个叫 .htaccess 的文件,并且将下面的文本复制进去 RewriteEngine On RewriteBase /api RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_FILENAME} !-f RewriteRule ^(.+)$ index.php [QSA,L] 我们告诉服务器,将所有指向 /api 的请求都转发到 index.php 文件 现在,所有的 url 都指向 index.php 了,比如下面的 url 都是指向 index.php 的 api/users api/users/10 api/users/5 现在我们同时解决了这两个问题 所有的 url 都被一个文件处理 url 都很优雅,结尾处没有 .php URI 但如何知道用户请求的是哪个 uri 呢? 很简单,使用 $_SERVER 超全局变量 让我们来看一些例子 // url api/users echo $_SERVER['REQUEST_URI']; // /api/users // url api/users/5 echo $_SERVER['REQUEST_URI']; // /api/users/5 // url api echo $_SERVER['REQUEST_URI']; // /api 看见了吗?这就是我们所需要的 现在,使用一个简单的 if 或者 switch 语句,我们就可以处理不同的路径了 如果你从来没有用过这些语句,去读这篇文章。 请求方法 接下来,我们需要从请求中获取请求的方法,以查看它是GET、POST、PUT、PATCH还是DELETE。 你可以从 $_SERVER 超全局数组中获取这个信息。 $_SERVER['REQUEST_METHOD'] 让我们将这两个值存储在变量中: $uri = $_SERVER['REQUEST_URI']; $method = $_SERVER['REQUEST_METHOD']; 我们可以在一个简单的 switch 语句中使用这两个变量来处理不同的请求。 我们需要判断以下请求 api/users 的 GET 请求 api/users/{id} 的 GET 请求 api/users 的 POST 请求 api/users/{id} 的 PUT 请求 api/users/{id} 的 DELETE 请求 让我们编写针对上述请求的 switch 语句 switch ($method | $uri) { /* * Path: GET /api/users * Task: show all the users */ case ($method == 'GET' && $uri == '/api/users'): break; /* * Path: GET /api/users/{id} * Task: get one user */ case ($method == 'GET' && preg_match('/\/api\/users\/[1-9]/', $uri)): break; /* * Path: POST /api/users * Task: store one user */ case ($method == 'POST' && $uri == '/api/users'): break; /* * Path: PUT /api/users/{id} * Task: update one user */ case ($method == 'PUT' && preg_match('/\/api\/users\/[1-9]/', $uri)): break; /* * Path: DELETE /api/users/{id} * Task: delete one user */ case ($method == 'DELETE' && preg_match('/\/api\/users\/[1-9]/', $uri)): break; /* * Path: ? * Task: this path doesn't match any of the defined paths * throw an error */ default: break; } 当我们想要在 switch 语句中使用两个变量,我们可以使用 | 符号 如果你想知道 preg_match 是如何工作的,看这篇文章。 数据库 现在是说说数据。储存数据的最好方法是将数据储存在数据库中。但在这篇教程中,我不想使用数据库。因此,我们使用一个 json 文件当作数据库来保证数据的持久性。 我的 json 文件看起来长成这个样子: { "1": "Pratham", "2": "Amir" } 如果你想知道如何使用 json,看这篇文章 我加载 json 数据并将其转换为数组,然后在 php 使用他们。如果我想要更改数据,我将数组转换回 json 并将其重新写入文件。 要将整个文件作为一个字符串读取并存储在变量中,我使用: file_get_contents($jsonFile); 而要将json写入文件,我使用: file_put_contents($jsonFile, $data); 好了,现在我们的数据库处理好了,让我们开始处理所有的路径。 我使用 Postman 发送请求并查看响应。 获取所有用户 case ($method == 'GET' && $uri == '/api/users'): header('Content-Type: application/json'); echo json_encode($users, JSON_PRETTY_PRINT); break; 获取单个用户 case ($method == 'GET' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); // get the id $id = basename($uri); if (!array_key_exists($id, $users)) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } $responseData = [$id => $users[$id]]; echo json_encode($responseData, JSON_PRETTY_PRINT); break; basename($uri) 会将 uri 的最后一部分给我。比如一个 api/users/10 这样的路径,它会返回 10. 然后我使用 array_key_exists 检查是否存在一个 id 为 10 的用户 添加一个新用户 case ($method == 'POST' && $uri == '/api/users'): header('Content-Type: application/json'); $requestBody = json_decode(file_get_contents('php://input'), true); $name = $requestBody['name']; if (empty($name)) { http_response_code(404); echo json_encode(['error' => 'Please add name of the user']); } $users[] = $name; $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user added successfully']); break; 我使用 file_get_contents('php://input') 以获取请求的 body 部分。由于在这个例子中我使用的是 json,我将会解码 json 以便我可以获取到名字。 更新一个用户 case ($method == 'PUT' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); // get the id $id = basename($uri); if (!array_key_exists($id, $users)) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } $requestBody = json_decode(file_get_contents('php://input'), true); $name = $requestBody['name']; if (empty($name)) { http_response_code(404); echo json_encode(['error' => 'Please add name of the user']); } $users[$id] = $name; $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user updated successfully']); break; 删除一个用户 case ($method == 'DELETE' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); // get the id $id = basename($uri); if (empty($users[$id])) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } unset($users[$id]); $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user deleted successfully']); break; 最终文件 现在我们的 index.php 文件看起来是这样的 在 70 行左右的代码中,我们使用 PHP 创建了一个 RESTful API,很神奇吧? <?php $jsonFile = 'users.json'; $data = file_get_contents($jsonFile); $users = json_decode($data, true); $uri = $_SERVER['REQUEST_URI']; $method = $_SERVER['REQUEST_METHOD']; switch ($method | $uri) { case ($method == 'GET' && $uri == '/api/users'): header('Content-Type: application/json'); echo json_encode($users, JSON_PRETTY_PRINT); break; case ($method == 'GET' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); $id = basename($uri); if (!array_key_exists($id, $users)) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } $responseData = [$id => $users[$id]]; echo json_encode($responseData, JSON_PRETTY_PRINT); break; case ($method == 'POST' && $uri == '/api/users'): header('Content-Type: application/json'); $requestBody = json_decode(file_get_contents('php://input'), true); $name = $requestBody['name']; if (empty($name)) { http_response_code(404); echo json_encode(['error' => 'Please add name of the user']); } $users[] = $name; $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user added successfully']); break; case ($method == 'PUT' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); $id = basename($uri); if (!array_key_exists($id, $users)) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } $requestBody = json_decode(file_get_contents('php://input'), true); $name = $requestBody['name']; if (empty($name)) { http_response_code(404); echo json_encode(['error' => 'Please add name of the user']); } $users[$id] = $name; $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user updated successfully']); break; case ($method == 'DELETE' && preg_match('/\/api\/users\/[1-9]/', $uri)): header('Content-Type: application/json'); $id = basename($uri); if (empty($users[$id])) { http_response_code(404); echo json_encode(['error' => 'user does not exist']); break; } unset($users[$id]); $data = json_encode($users, JSON_PRETTY_PRINT); file_put_contents($jsonFile, $data); echo json_encode(['message' => 'user deleted successfully']); break; default: http_response_code(404); echo json_encode(['error' => "We cannot find what you're looking for."]); break; } 额外内容 在这种情况下,我不希望删除我的所有用户,所以我加了一个新的语句,如果只剩下最后一个用户,它将不会被删除,像这样 if (sizeof($users) == 1){ http_response_code(404); echo json_encode(['error' => 'there is only one user left. you cannot delete it!']); break; } 源码 你可以在原作者的 github 上看到完整注释的源代码以及 post man 集合 总结 现在你知道如何在 PHP 中创建一个简单的 RESTful API。 我推荐你打开一个 PHP 文件并复习所有的这些我们进行的步骤,并且像本文一样添加一些额外的资源 如果你有任何的建议、问题或者观点,请联系文章原作者,他期待着听到你的声音。 要点 不使用框架,用 PHP 创建一个 RESTful API 在 PHP 中使用优雅的 URL 处理请求的 body 使用 Json 文件作为你的数据库 使用多个变量作为 switch 的关键词

2023/12/12
articleCard.readMore

在 Hexo Fluid 主题中使用霞鹜文楷

我的博客换到 fluid 主题已经有两年了,期间一直有为博客更换字体的想法,但之前没有前端开发的相关知识支撑我换字体的需求。不过现在,我已经有了一些 Vue.js 的开发经验,相信能支撑我完成这个目标。 我在谷歌搜索到了这篇文章——《Hexo博客Fluid主题,字体全局更改为霞鹜文楷体》。 文章中直接修改了 themes/fluid/layout/_partial/head.ejs 让文章生成时在 html 的 head 标签中引入 lxgw-wenkai-screen-webfont 的 css 文件,并使用自定义 css 方案。但这种方案我不喜欢,我的 fluid 主题是通过 npm 安装 hexo-theme-fluid 的方式引入的,这意味着我不能直接编辑 themes/fluid 下的文件,包括文章中需要编辑的 head.ejs 和 _config.yml 。 我翻阅了 lxgw-wenkai-webfont 的 README,找到了使用 cdn 引入的方式。我们需要在 html 的 head 标签中加上下面这段: <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/lxgw-wenkai-screen-webfont@1.1.0/style.css" /> 但我注意到我想要的 lxgw-wenkai-screen-webfont 在 staticfile.org 上也有 cdn 提供,且该 cdn 有海外节点,是不错的选择,所以我要通过下面这段引入: staticfile 已经因为供应链投毒被各 adblock 插件屏蔽,已改用 npmmirror <link rel="stylesheet" href="https://registry.npmmirror.com/lxgw-wenkai-screen-web/latest/files/style.min.css" /> 但要如何引入呢? 在 hexo 的官方文档中,我找到了一个方案。可以在博客的 workdir 下创建一个 scripts 文件夹,在当中放入需要执行的 js 脚本。 在这篇名为《Fluid -23- 添加 Umami 统计》 的文章里,我找到了在 hexo 生成静态文件时直接注入的方式。 在 scripts/font.js 中写入: hexo.extend.injector.register('head_end', '<link rel="stylesheet" href="https://registry.npmmirror.com/lxgw-wenkai-screen-web/latest/files/style.min.css" />', 'default'); 这样一来,字体文件的 css 便被我们成功引入了,我们还需要指定页面使用霞鹜文楷作为默认字体。 在 fluid 主题的配置文件 _config.fluid.yml 中,有一个名为 font-family 的配置项,直接写上 font-family: "LXGW Wenkai Screen" 便可大功告成。

2023/11/28
articleCard.readMore

【翻译】GLWTPL——祝你好运开源许可证

说实话,当我第一次看见 GLWTPL( Good Luck With That Public License ) 的时候,我差点把嘴里的饭给喷出来了,这是一个非常有意思的开源许可证。原文请直接戳原仓库 -> https://github.com/me-shaon/GLWTPL 如果你对你的代码有这样的感觉: 当我写下这段代码的时候,只有上帝和我知道我在写什么。 现在只有上帝知道了。 那不如来考虑一下将这份开源许可证添加到你的项目中! 并且,祝你未来的自己、人类同胞、外星人或人工智能机器人(可以编码并会毁灭人类)——实际上是任何敢于参与你的项目的人好运。 当然,它还有一个脏话版本。干杯! 可能的使用场景 你写了一些你并不为此自豪的代码,但你想要将它开源。 你想要将你写的代码“放生”,但不想为此负任何责任。 “无论如何我都已经写完了”,并且你没有时间/意图对你的代码进行修复、修改或改进。 想要将自己参加的黑客马拉松/代码竞赛的代码打造成一个爆火的仓库?该使用什么开源许可证?这就是为你量身打造的开源许可证! 你的大学课设或科研工作与这份许可证是天作之合。 一些翻译版本 Albanian - Shqip Arabic - العربية Bangla - বাংলা Cantonese - 廣東話 Catalan - Català Croatian - Hrvatski Danish - Dansk Dutch - Nederlands French - Français Galician - Galego Georgian - ქართული German - Deutsch Greek - Ελληνικά Hebrew - עברית Indonesian - Bahasa Indonesia Italian - Italiano Japanese - 日本語 Korea - 한국어 Latvian - Latviski Portuguese - Português (BR) Russian - Русский Simplified Chinese - 简体中文 Spanish - Español Swedish - Svenska Traditional Chinese - 正體中文 Turkish - Türkçe Vietnamese - Tiếng Việt 本译文翻译于 2023 年 11 月 12 日,日后大概率也不会对本文进行任何改进,故也采用 GLWTPL 向所有人授权。 附 此协议的英文原版: GLWT(Good Luck With That) Public License Copyright (c) Everyone, except Author Everyone is permitted to copy, distribute, modify, merge, sell, publish, sublicense or whatever they want with this software but at their OWN RISK. Preamble The author has absolutely no clue what the code in this project does. It might just work or not, there is no third option. GOOD LUCK WITH THAT PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION 0. You just DO WHATEVER YOU WANT TO as long as you NEVER LEAVE A TRACE TO TRACK THE AUTHOR of the original product to blame for or hold responsible. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Good luck and Godspeed. 此协议在 Github 上的中文翻译版本: GLWT(Good Luck With That,祝你好运)公共许可证 版权所有© 每个人,除了作者 任何人都被允许复制、分发、修改、合并、销售、出版、再授权或 任何其它操作,但风险自负。 作者对这个项目中的代码一无所知。 代码处于可用或不可用状态,没有第三种情况。 祝你好运公共许可证 复制、分发和修改的条款和条件 0 :在不导致作者被指责或承担责任的情况下,你可以做任何你想 要做的事情。 无论是在合同行为、侵权行为或其它因使用本软件产生的情形,作 者不对任何索赔、损害承担责任。 祖宗保佑。

2023/11/12
articleCard.readMore

通过巴法云将向日葵智能插座接入米家,实现小爱同学远程控制

在上一篇博客中,我们介绍了如何在本地局域网中通过发送 http 请求控制向日葵智能插座 C2 的开关状态。但这还远远不够,我自己是小米生态链的忠实用户,在宿舍里也接入了四五个米家的智能设备,因此我想把这个智能插座接入米家,实现离家时一键关闭。 在阅读小米IoT开发者平台的接入文档后,我发现米家对于个人开发者并不友好,接入文档大部分要完成企业认证以后才能实现。在谷歌一番搜索过后,我发现了通过假设 Home Assistant 后通过巴法云接入米家的方案。但我眼下就这一个非米家的智能家具,暂时还不想去碰 Home Assistant 那套体系。 因此我便找上了巴法云。在巴法云的官网中提到,他们是「专注物联网设备接入&一站式解决方案」,对于个人开发者,目前平台免费使用。网站的文档虽然并不优雅美观,却透露出实用主义的气息,针对接入提供了 TCP 长连接和 MQTT 两种方案,看着就很适合实现我的需求。 在巴法云文档中的「五分钟入门」那一栏介绍了远程控制的业务逻辑: 如果单片机订阅了一个主题,手机往这个主题推送个消息指令,单片机由于订阅了这个主题,就可以收到发往这个主题的消息,就可以达到手机控制单片机的目的。 所以我需要在巴法云的控制台创建一个针对于智能插座的主题,让我局域网内的一台设备订阅这个主题。接入米家以后,米家需要控制向日葵的智能插座时就向巴法云的这个主题推送一条消息,局域网内的设备就能接收到推送消息,进而调用智能插座的 api 实现远程开关。在这里,我选择使用一台刷了 Armbian 的 N1 作为局域网内的转发器。整个控制流程看上去是下面这个样子: 我并不知道 tcp 长连接的数据传输应该如何实现,但看起来 MQTT 是一个比较成熟的协议,因此我选择使用 MQTT 作为巴法云和 N1 之间的通讯协议。 在巴法云的控制台,选择 MQTT 设备云,创建一个新的主题,注意需要以 001~009 结尾,否则在米家里看不见创建的这个主题。 当主题名字后三位是001时为插座设备。 当主题名字后三位是002时为灯泡设备。 当主题名字后三位是003时为风扇设备。 当主题名字后三位是004时为传感器设备。 当主题名字后三位是005时为空调设备。 当主题名字后三位是006时为开关设备。 当主题名字后三位是009时为窗帘设备。 当主题名字为其他时,默认为普通主题节点,不会同步到米家。 此时,我便可以在手机的米家中找到巴法云并接入这个插座。 至此,米家那边的接入已经完成了,虽然没法在米家中找到对应设备的卡片,但是可以在小爱同学的小爱训练计划中找到对应的设备。 我们还需要让本地的 N1 盒子使用 MQTT 协议订阅巴法云的消息。 参考代码如下: #!/usr/bin/python3 import paho.mqtt.client as mqtt import requests # 智能插座相关 host = '' sn = '' key = '' time = '' # 巴法云相关 client_id = '' theme = '' def set_adapter_status(status: bool): url = 'http://' + host + '/plug' requests.get(url, params={ "status": 1 if status else 0, "sn": sn, "key": key, "_api": "set_plug_status", "time": time, "index": 0 }) def on_connect(client, userdata, flags, rc): print("Connection returned with result code:" + str(rc)) client.subscribe(theme, qos=1) def on_message(client, userdata, msg): if msg.payload.decode("utf-8") == 'on': set_adapter_status(True) elif msg.payload.decode("utf-8") == 'off': set_adapter_status(False) def on_subscribe(client, userdata, mid, granted_qos): print("Subscribed: " + str(mid) + " " + str(granted_qos)) client = mqtt.Client(client_id=client_id, clean_session=False, protocol=mqtt.MQTTv311) client.on_connect = on_connect client.on_message = on_message client.on_subscribe = on_subscribe client.connect("bemfa.com", 9501, 60) client.loop_forever() 参考链接 巴法开放平台 Python MQTT客户端 paho-mqtt Python MQTT 客户端对比

2023/11/2
articleCard.readMore

使用 Root 后的安卓手机获取向日葵智能插座 C2 的开关 api

之前看到 https.gs 上的一篇文章,发现可以抓取向日葵智能插座 C1Pro 的开关 api,并实现局域网或公网的控制。这样一来,我们其实就不需要依赖于向日葵自己家的 App 去实现智能插座的开关操作,还是比较方便的。今年趁着双十一,直接低价拿下来带有计电功能的 C2,便也来试一试能不能抓到接口。 首先,拿到插座以后肯定还是下载向日葵的官方 App,完成 wifi 的链接,这里就不再赘述。 然后就可以打开我们的抓包软件。需要注意的是,原博客中抓到的接口是 http 协议,但这个接口在新版的 App 上已经变为了 https 协议,因此我们需要找一台 Root 过后的安卓机去抓包。抓包的步骤没什么好说的,用 Root 权限给本地安装自己的 CA 证书,然后打开抓包模式,在向日葵的 App 那边开关几次插座,回来就能看到这一段时间内的请求。 点开可以看到,这是一个 GET 请求,一共有如下几个参数 status 这是状态设置,设置为 1 时为打开指令,0 为关闭指令 sn 这个应该是设备码 key 应该是用来操作设备的密钥 _api 操作类型,我只关心插座的打开关闭,所以设为 set_plug_status 即可 time 奇奇怪怪的而参数,也不是 unix 时间戳,反正照抄就行了 index 原博说是用来给插排操作指定第几个孔位的,我们智能插座直接设置为 0 即可 理论上你用抓出来的 url 已经可以实现公网访问了,但我测试下来并不行,可能是向日葵那边的服务器做了别的校验,比如说判断了 ua 之类的?不过无所谓,我本来就是打算局域网内操作。 登陆路由器后台,寻找疑似智能插座的设备,一般很容易就能找到。 使用 nmap 命令扫对应 ip 开放的端口。不知道是不是巧合,我和原博扫出来的端口都是 6767 端口。 将上面抓到的 url 的域名换成 ip:port,https 协议改成 http 协议,在浏览器中直接访问,获得了 0 的状态码,插座也正常开关。

2023/11/1
articleCard.readMore

创建 b23.tv 追踪参数移除 bot

前两天似乎有人高调宣称自己发 b23.tv 没问题,结果过两天就被拿下的消息。我自己并不是他的粉丝,但这个戏剧性的流言也又一次说明了注重隐私保护的重要性。 早前就有 b23.tf 和 b23.wtf 两个域名专门在做移除追踪参数的事情。只要将短链接中的 b23.tv 改成 b23.tf ,别人访问链接时就会被转到移除了追踪参数的链接。但这需要发送者在分享时手动更改域名。 因此,我也开始为自己的 bot 添加了 b23.tv 的 track id 移除功能。当用户的信息中包含 b23.tv 短链接,将会自动发送一条移除了 track id 的信息,用户就可以直接点击无追踪参数的链接。 当然,这两种方案并不能保护链接分享者的个人信息泄漏,因为 b23.tv 后面的参数是可以被别人看到的,通过这些参数就可以定位到链接分享者的个人信息,所以不能防止群里的内鬼倒查分享者的个人信息,但起码可以阻止大数据算法对群里的几个人产生关联。 b23 短链接将会泄漏哪些个人信息? 通过 curl 命令,我们就可以看到 b23.tv 短链接重定向到了哪个页面。 这是所携带的 GET 请求参数 'buvid': ['*************************************'], 'from_spmid': ['tm.recommend.0.0'], 'is_story_h5': ['false'], 'mid': ['************************'], 'p': ['1'], 'plat_id': ['116'], 'share_from': ['ugc'], 'share_medium': ['android'], 'share_plat': ['android'], 'share_session_id': ['************************************'], 'share_source': ['GENERIC'], 'share_tag': ['s_i'], 'spmid': ['united.player-video-detail.0.0'], 'timestamp': ['**********'], 'unique_k': ['*******'], 'up_id': ['*********'] 其中,我替换成星号的部分都是有可能涉及到信息泄漏的部分,甚至没打码的部分也可以用来推测你的平台信息。 QQ Bot 尽管目前腾讯针对 go-cqhttp 的封杀力度挺大的,但我还在用。 在 QQ 中的 b23.tv 追踪参数移除主要有两个方面。一是用户发送的消息中可能含有 b23.tv 短链接,二是用户在手机端直接调用 bilibili 自带的「分享到QQ」的功能,这样的话在 QQ 中会显示为小程序,go-cqhttp 接收到的是一个 json 的 CQ Code。 针对第一种情况,处理起来就相对简单,首先判断用户的信息中是否存在 b23.tv 这一关键词,然后用正则表达式获取完整的 b23 链接,再使用 python 的 requests 库去请求对应链接,返回带有明文追踪参数的 url 后去除 GET 参数即可。 参考代码如下 if 'https://b23.tv' in message: pattern = r'https://b23\.tv/[^\s]+' urls = re.findall(pattern, message) ret = 'TrackID removed:' for i in urls: ret = ret + '\n' + b23_to_bvid(i) def b23_to_bvid(url): tracked_url = requests.get(url,allow_redirects=False).headers['location'] return tracked_url.split('?', 1)[0] 而针对第二种情况,则需要先解析对应的 json 信息,再参考第一种方法获取无追踪参数的链接。 参考代码如下 if message.startswith('[CQ:json,data') and 'b23.tv' in message: decoded_data = html.unescape(message) match = re.search(r'\[CQ:json,data=(\{.*?\})\]', decoded_data) json_str = match.group(1) json_data = json.loads(json_str) if json_data['meta'].get('detail_1') is not None: raw_url = json_data['meta'].get('detail_1').get('qqdocurl') elif json_data['meta'].get('news') is not None: raw_url = json_data['meta'].get('news').get('jumpUrl') clean_url = b23_to_bvid(raw_url) def b23_to_bvid(url): tracked_url = requests.get(url,allow_redirects=False).headers['location'] return tracked_url.split('?', 1)[0] TG Bot 这个平台是提供了 Bot 的 API 的,所以也不用担心会被官方封杀,可惜用户访问起来可能相对困难,也不能要求所有联系人都迁移到这个平台上。思路也是一样的,用 requests 去请求 b23 短链,返回去除跟踪参数的 url。 参考代码如下 from telegram import Update from telegram.ext import ApplicationBuilder, ContextTypes, CommandHandler, MessageHandler, filters import requests import re ua = 'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0' async def start(update: Update, context): await context.bot.send_message(chat_id=update.effective_chat.id, text="Hello World!") async def b23_remover(update: Update, context): seng_msg = 'TrackID removed:' if 'https://b23.tv' in update.message.text: pattern = r'https://b23\.tv/[^\s]+' urls = re.findall(pattern, update.message.text) for i in urls: seng_msg += '\n' + await b23_to_bvid(i) await context.bot.send_message(chat_id=update.effective_chat.id, text=seng_msg) async def b23_to_bvid(url): tracked_url = requests.get(url,allow_redirects=False,headers={'User-Agent': ua}).headers['Location'] return tracked_url.split('?', 1)[0] start_handler = CommandHandler("start", start) b23_remove_handler = MessageHandler(filters.TEXT, b23_remover) if __name__ == '__main__': TOKEN='**********************************************' application = ApplicationBuilder().token(TOKEN).build() application.add_handler(start_handler) application.add_handler(b23_remove_handler) application.run_polling() 代码编写参考了 柯罗krau的博客 | krau's blog,使用时请注意以下问题: 你的机子是否拥有能访问到对应的 api 的网络环境 botfather 那边是否打开了 allow group botfather 那边是否关闭了 privacy mode

2023/10/29
articleCard.readMore

jinja2 中如何优雅地实现换行

在使用 python 的 jinja2 模板引擎生成 html 的时候,会遇到 \n 换行符无法被正常换行的问题。我本能的想法就是将 \n 替换成 html 标签 <br />,但失败了,jinja2 有自动转义的功能,直接将标签原模原样地渲染了出来,并没有生效。而为这一段代码块关闭自动转义则会有被 js 注入的风险,因此这也不是上策。 在 jinja2 的官方文档中,提出了使用 filter 的方案。也就是说,filter 将 \n 识别出来,并自动替换成 <br /> 标签,并且使用 Markup 函数将这一段 html 文本标记成安全且无需转义的。见: https://jinja.palletsprojects.com/en/3.1.x/api/#custom-filters import re from jinja2 import pass_eval_context from markupsafe import Markup, escape @pass_eval_context def nl2br(eval_ctx, value): br = "<br>\n" if eval_ctx.autoescape: value = escape(value) br = Markup(br) result = "\n\n".join( f"<p>{br.join(p.splitlines())}<\p>" for p in re.split(r"(?:\r\n|\r(?!\n)|\n){2,}", value) ) return Markup(result) if autoescape else result 使用这段代码后,我遇到了连续两个换行符被识别成一个换行符的问题,依然不满意。 在 issue#2628 中,我找到了一个相对优雅的解决方案——使用 css 样式来完成这个任务。 通过设置 white-space: pre-line; 的 css 样式,html 在被渲染时将会不再忽略换行符,浏览器就能够在没有 br 标签标注的情况下实现自动换行。而如果设置为 white-space: pre-wrap; 则多个空格将不会再被合并成一个空格,直接治好了我在入门 html 时的各种不适。 此外,通过 word-break: break-word; 的 css 样式可以实现只有当一个单词一整行都显示不下时,才会拆分换行该单词的效果,可以避免 break-all 拆分所有单词或者 normal 时遇到长单词直接元素溢出的问题。

2023/9/3
articleCard.readMore

手动指定 python-selenium 的 driver path 以解决在中国大陆网络环境下启动卡住的问题

之前因为学校社团迎新的需求,就临时写了一个 QQ Bot,最近又给 bot 加上了 /q 的功能,原理是通过 python 的 selenium 去启动一个 headless Firefox 去截由 jinja2 模板引擎生成的 html 的图。 每次这个 bot 重启的时候都因为 selenium 而需要花费好几秒的时间,甚至经常概率性启动失败。我就寻思者应该把这个图片生成的 generator 从 bot 中抽出来,这样就不至于每次重启 bot 都要遭此一劫。但就在我将 generator 打包成 docker 部署上云服务器的时候,发现居然无法启动。于是手动进 docker 的 shell 开 python 的交互式终端,发现在创建 firefox 的 webdriver 对象的时候异常缓慢,等了半分钟以后蹲到一个报错如下: Traceback (most recent call last): File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/common/driver_finder.py", line 38, in get_path path = SeleniumManager().driver_location(options) if path is None else path ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/common/selenium_manager.py", line 95, in driver_location output = self.run(args) ^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/common/selenium_manager.py", line 141, in run raise WebDriverException(f"Unsuccessful command executed: {command}.\n{result}{stderr}") selenium.common.exceptions.WebDriverException: Message: Unsuccessful command executed: /usr/local/lib/python3.11/site-packages/selenium/webdriver/common/linux/selenium-manager --browser firefox --output json. {'code': 65, 'message': 'error sending request for url (https://github.com/mozilla/geckodriver/releases/latest): connection error: unexpected end of file', 'driver_path': '', 'browser_path': ''} The above exception was the direct cause of the following exception: Traceback (most recent call last): File "<stdin>", line 1, in <module> File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/firefox/webdriver.py", line 59, in __init__ self.service.path = DriverFinder.get_path(self.service, options) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.11/site-packages/selenium/webdriver/common/driver_finder.py", line 41, in get_path raise NoSuchDriverException(msg) from err selenium.common.exceptions.NoSuchDriverException: Message: Unable to obtain driver for firefox using Selenium Manager.; For documentation on this error, please visit: https://www.selenium.dev/documentation/webdriver/troubleshooting/errors/driver_location 我才发现 selenium 试图访问 https://github.com/mozilla/geckodriver/releases/latest 且访问失败了。仔细阅读了 selenium 项目的文档发现新版本的 selenium 会尝试自动下载 webdriver: As of Selenium 4.6, Selenium downloads the correct driver for you. You shouldn’t need to do anything. 表面上看上去我不需要做任何事情,但项目组忽略了中国大陆的网络环境。 服务是要在境内的云服务器上跑的,我也不敢开代理,现在比较靠谱的方案是我去手动指定 Firefox 的 geckodriver,避免 selenium 去帮我自动下载一份。在 python-selenium 的官方文档中是让我们创建 Firefox 的 webdriver 时去传入一个 executable_path='geckodriver' 的关键词参数,可惜这是过时的用法,应该是维护者还没来得及更新文档。 随后便在 stackoverflow 上找到了新版 selenium 手动指定 Chrome 的 chromedriver 的方法 from selenium import webdriver from selenium.webdriver.chrome.service import Service service = Service(executable_path='chromedriver.exe') options = webdriver.ChromeOptions() driver = webdriver.Chrome(service=service, options=options) # ... driver.quit() 原文给的是 chrome 的方案,但 Firefox 的方案基本也是一致的,应该也是去创建一个 service 对象,猜一猜就能猜到。 from selenium import webdriver from selenium.webdriver.firefox.service import Service from selenium.webdriver.firefox.options import Options service = Service(executable_path='/root/geckodriver') # 我这里是 docker 打包,懒得创建一个普通用户了,就直接用了 root 用户的 home 目录 options = Options("--headless") driver = webdriver.Firefox(service=service, options=options) # ... driver.quit() 手动指定 geckodriver 后,在我 1 核 1 G 的小主机上创建 webdriver 对象,基本都可以秒完成。

2023/9/2
articleCard.readMore

从零开始的静态网页部署(到个人云服务器)

这篇博客是受 Tiancy 之托,在2023年精弘网络暑期授课的前端系列第七节课时针对项目部署这一块内容时所产生的产物。在授课视频中,受时长所限,我不得不采用宝塔面板+纯 ip 访问的方式来完成一个简单的部署,但这终究不是什么优雅的方案: 宝塔的安全性堪忧、其隐私性也是备受争议,而纯 ip 访问的方式也过于简陋,且没有支持 https 访问。 因此这篇博客将以面对初学者的口吻去讲述如何从零开始部署一个 Vue.js 的项目到云服务器,以解我心头的愧疚。但是,我没有备案过的域名,且国内云服务器厂商众多,这篇博客终究不可能做到像保姆级教学那样去一一演示每一家云服务器厂商网页面板上的操作过程,而一些比较基础的概念我会给出简单的解释和例子以及引用一些外部链接,但终究不会全面覆盖到,诸位还请见谅。 本文采用了一些 ChatGPT 和 Google Bard 提供的内容,准确性经过我本人核阅。 基础 Web 知识 针对以下三个知识点,我是在初中的信息课上学到的,互联网上应该不乏对于这三个问题的权威解释,因此我也不在此赘述,不知道的小伙伴请自行搜索。 ip 地址是什么 域名是什么 DNS 服务器是干什么的 关于备案 不备案的影响 当你通过域名去访问境内服务器的 80 (http 默认端口) 和 443 (https 默认端口)时,如果该域名没有备案或者境内这台云服务器的云服务器商不知道你在别的服务商那里有备案的情况下,则会对请求进行拦截。对于访问 80 的请求,将会直接劫持 http 请求以重定向到他们的备案提示页面;对于访问 443 的请求,由于 https 没法被劫持,则会通过连接重置的方式阻止你访问。如果你确定你需要使用中国大陆境内的云服务器,应当采取「备案」和「接入备案」两种方式分别解决上述两种情况。 备案方法 每个省都有自己对应的管局,而各省的管局对于备案的规则都有些差异,而个人备案一般是找自己户籍所在地的管局去备案,详细的可以看阿里云写的文档。 使用中国大陆境外的云服务器 可以选择和我一样去中国大陆以外的地区部署云服务,但由于众所周知的原因,访问别的国家或地区的服务器可能会有速度慢、延迟高等问题,这涉及到线路优化,也比较复杂。更糟糕的情况是,你甚至有可能刚开出来一台机子就发现这个 ip 在中国大陆境内是无法访问到的,这也是比较尴尬的地方。一般来说,可以选择在境内的云服务器商那里实名认证(不是备案)去购买他们的境外服务器(比如 ucloud 新用户优惠的香港云服务器,ucloud 的客户经理看到了能不能再送我一台机子啊),这种机子是线路相对比较好的。 选购中国大陆境外的云服务器时,厂商可能会提供测试 ip 来帮助你判断线路质量,可以使用 ipip.net 提供的 besttrace 程序来查看数据包经过的地方,很可能你买一台香港的服务器,数据却要从日本或者美国绕一圈,这就非常尴尬。 域名部分 域名注册 要获得一个域名,最简单的方式是花钱。境内的阿里云、腾讯云、华为云等几家比较有名的云服务器厂商均有域名注册的业务且价格基本差距不大,可以随便找一个注册。而境外的域名注册商,我这边个人推荐 namesilo,这家支持支付宝付款且价格尚可,首次购买前可以去搜索引擎搜一搜近期的优惠码,可能会有一些优惠折扣。(可恶我没有拿到 aff 回扣) 域名解析 域名解析的作用 如果你了解了 dns 的作用,那我们可以来简单讲讲域名解析是干什么的。dns 服务器将会告诉用户的设备某一个域名它对应的 ip 是多少,而域名解析这一步就是告诉世界上所有的 dns 服务器这个域名从此刻开始对应的 ip 是多少,以便世界上所有的 dns 服务器向网民在需要时告知他们正确的 ip 地址。 要实现这一步骤并不复杂,作为初学者我们也不必去担心会不会有人把你花钱买来的域名指向错误的 ip 地址,这些都交给域名解析服务去解决。几乎每一家提供的域名解析服务页面上都会指导你去将域名的 NameServer 设置为他们家的服务器,这里也不做教学。 域名解析服务推荐 凡是提供域名注册服务的云服务商基本也都会提供域名解析服务,在这里我主要推荐两家云服务商(我没拿广告费啊)—— cloudflare 和 dnspod。这两家免费版套餐的操作页面都简洁明了,没有非常扎眼的广告。前者提供了除中国大陆以外地区的 cdn 加速服务,而后者可以提供境内境外分线路解析的功能(把来自境内的用户指向 ip 地址 A,来自境外的用户指向 ip 地址 B)。 解析记录类型 作为初学者只需要了解 A 记录和 CNAME 记录就行了。 A 记录的意思就是将一个域名指向一个 ipv4 地址,也就是去实现 dns 服务器最主要的作用。而 CNAME 记录是将一个域名指向另一个域名,通俗来讲就是「和它一样」。比如 a.com 如果 CNAME 指向 b.com,意思就是说我现在不确定 a.com 的 ip 是多少,但我知道 a.com 的 ip 和 b.com 一样,所以你去查 b.com 就行了。 服务器部分 云服务器的购买 这部分我直接忽略过去了,本文在「关于备案」这一部分已经详细阐述了备案相关的内容,购买中国大陆境内还是境外的服务器需要由屏幕前的各位自己决定~~(应该没人会把我的博客打印成纸质稿看吧)~~。 如何选择云服务器上要运行的 Linux 发行版 服务器上常用的 Linux 发行版主要是 Debian、Ubuntu、CentOS(这个死得差不多了) 这三个,那我个人更熟悉的是 Ubuntu,版本号越新越好,截止本文发出最新的 lts 版本是 22.04 lts,所以直接选择这个就行。 使用 ssh 连接上服务器 在云服务器的网页面板上选择好服务器的配置与运行的操作系统后,云服务商应该至少给你提供两样东西: 云服务器的 ip 和 root 用户的登陆密码。这可能是在网页面板上展示的,一些境外的云服务商可能是直接发送到你注册时预留的邮箱中的,这都无所谓。拿到这两样东西我们就可以使用 ssh 连接到服务器的终端,进行配置操作。 打开自己系统的终端,使用如下命令去连接云服务器(Win10 以上的系统应该也已经自带 openssh 了) ssh root@<your_server_ip> [zhullyb@Archlinux ~]$ ssh root@120.55.63.96 The authenticity of host '120.55.63.96 (120.55.63.96)' can't be established. ED25519 key fingerprint is SHA256:Op8u4Fv+NvtOxJDKeBQ/jIsFpuR4EYTUt53qjG8k6ok. This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])? yes Warning: Permanently added '120.55.63.96' (ED25519) to the list of known hosts. root@120.55.63.96's password: Welcome to Ubuntu 22.04.2 LTS (GNU/Linux 5.15.0-78-generic x86_64) 输入密码时,已经输入的密码部分在屏幕上不会显示,但无需理会,只要将云服务器的密码粘贴后直接敲回车就好。 如何编辑一个服务器上的文件 一般来说,网上的教程会推荐你使用 vim 这个 tui 界面的编辑器去编辑这个文件,但 vim 的学习成本有点高,如果只是临时编辑服务器上的文件的话,我个人更加推荐使用 nano nano /etc/caddy/conf.d/example.conf 这行命令表示我要编辑 /etc/caddy/conf.d/example.conf 这个文件,如果这个文件不存在则去创建这个文件。 随后你可以根据自己的需求去编辑文件了,上下左右按键可以调整光标位置,直接敲键盘上的字母按键就可以把字母敲进去,想推出时使用 ctrl+s 保存,再使用 ctrl+x 退出就可以了。 云服务器的安全组规则 一般是国内的云服务厂商会有安全组规则这种东西,你可以理解成一个额外的防火墙。一般来说,80 和 443 两个端口被我们约定作为网页的默认端口,80 是 http 的端口,而 443 则是 https 的端口。因此,我们需要在安全组规则这里去允许 80 和 443 两个端口能被外部访问到。截图中是阿里云的控制面板。 云服务商给的默认规则应该是下面这个样子的: 这里开放的 22 端口用于 ssh 连接服务器,而3389 则是 Windows 的远程桌面。我们可以使用「快速添加」按钮来开放 80 和 443 端口 Linux 下常见的文件路径及对应作用 在这一章节中,我只罗列了几个比较常见的路径,更多的资料推荐查阅菜鸟教程,写得还不错。$USER 指当前用户的用户名 路径 作用 /home/$USER 用户的家目录,下有 Desktop,Download,Picture 等多个文件夹(root 用户的家目录是 /root) /etc 存放软件的配置文件的地方 /usr/bin 存放二进制可执行文件的地方,一般也会被链接到 /bin /usr/lib 一般用于存放依赖库(动态链接库),一般会被链接到 /lib /usr/share 一些共享数据,比如帮助文档、软件需要的资源文件等等 /opt optional(可选) 的缩写,一些由官网提供的(区别于发行版自带的)软件可能被安装到这里 /boot 开机引导使用的路径,一般在正常使用时不会去操作这里 /var variable(变量) 的缩写,存放那些经常被变更的东西,比如运行日志、网站数据等等 caddy 的配置与使用 caddy 是一个 web 服务器,他是使用 golang 写的一个平替品,拥有配置更简单、自动申请 Let's Encrypt 证书的优势,我个人非常推荐非专业运维去使用这个。caddy 的官方文档在 https://caddyserver.com/docs/ ,但我相信你们不会去看(我也没有认真看过),有问题可以尝试去问问 chatgpt 看看能不能得到想要的配置文件。caddy 现在已经迭代到 V2 版本了,与 V1 版本相比有一些语法差异以支持更多的功能,且许可证允许商用更加自由。 caddy V2 支持使用 json 配置文件或者 Caddyfile,对于不复杂的需求我个人更推荐后者,简洁易懂。下面是我博客所使用的 Caddyfile 示例: # 这里表示,使用 zhul.in 这个域名访问 443 端口时,提供以下内容 zhul.in:443 { # 这里设置了所需提供内容的目录 root * /var/www/blog # 这里设置的是开启 https 支持时所需要使用的 ssl 证书文件,但如果不设置也不碍事,caddy 会自动帮你申请 Let's Encrypt 的 ssl 证书 tls /var/www/key/zhul.in.cert /var/www/key/zhul.in.key # 这里表示我们开启了 zstd 和 gzip 两种压缩算法,来减少数据传输量,不设置也没问题 encode zstd gzip # 这里表示我们开启了一个文件服务器,当你访问 https://zhul.in/example_file 时,caddy 会提供 /var/www/blog/example_file 这个文件的内容 file_server # 这里是错误处理部分 handle_errors { # 这里表示当发生错误时,将请求重定向到 /404.html 这个文件 rewrite * /404.html # 这里使用了模板来处理错误页面。当发生错误时,Caddy会使用模板引擎来填充错误页面的内容,以便向用户显示有关错误的相关信息。 templates #这里表示继续使用文件服务器来提供错误页面 file_server } } 你可以发现,如果要把这个 Caddyfile 写到最简单,仅仅是能跑的状态,只需要这几行: zhul.in:443 { root * /var/www/blog file_server } 这就是我为什么推荐非专业运维去使用 caddy 的原因,只需要三行代码就可以跑起来一个简单的服务。 而部署一个 Vue.js 项目,我们可能会需要多加一行 try_files {path} {path}/ /index.html ,这一行代码的意思是当用户尝试访问 /example 时,实际需要用户的浏览器去访问 /index.html 这个地方,因为使用了 vue-router 的项目的编译产物只有 /index.html 而没有 /example.html,而后者的内容是包括在前者中的。以下的 Caddyfile 是精弘的首页正在使用的配置文件,应该可以适用于绝大多数的 Vue 项目: www.myzjut.org { root * /var/www/jh encode zstd gzip file_server try_files {path} {path} /index.html } 第一行省略了端口号,说明 80 和 443 端口都支持。 通过sftp/rsync将本地的静态网页上传到云服务器的对应目录 使用 sftp 部署 sftp 是一个交互性比较强的上下传工具,如果不喜欢背命令的话可以考虑使用 sftp,操作起来都比较顺其自然 首先,我们在本地 cd 到静态网页文件所在的路径,比如一个 Vue 项目编译产生的文件可能就会在 dist 下面 cd dist/ 然后,我们使用 sftp 连接到服务器,这和 ssh 命令没什么两样的,就换了个命令名。 sftp root@<your_server_ip> 输入 root 用户的密码后,命令行的提示符就会变成 sftp > 的样子 [zhullyb@Archlinux ~]$ sftp root@<your_server_ip> Connected to <your_server_ip>. sftp> 这是一个交互式的命令行窗口,可以使用 cd、mkdir 等几个简单的命令。我们先创建 /var/www 这个文件夹: sftp> mkdir /var/www 再创建 /var/www/jh 这个文件夹: sftp> mkdir /var/www/jh 随后,我们就可以进入远程服务器的 /var/www/jh 目录下 sftp> cd /var/www/jh 这样我们就可以把本地的静态网页文件上传到服务器,使用 put 命令即可,下面的命令表示将本地当前目录下的所有文件以及其子文件夹全部内容都上传到服务器的当前文件夹,也就是 /var/www/jh sftp> put -r * 再输入 exit 即可推出 sftp 状态。 这边再教一些 sftp 使用中的常用命令: ls: 查看远程服务器中当前目录中所有非隐藏文件 lls: 查看本地当前路径中的所有非隐藏文件 pwd: 查看远程服务器中当前的路径 lpwd: 查看本地当前的路径 使用 rsync 部署 rsync 的交互性就不太强,是在本机操作的,需要提前写好一行比较长的命令去执行操作,比较适合写在脚本里。 下面这行代码是我们精弘网络首页使用 github action 部署时的命令 rsync -avzP --delete dist/ root@<your_server_ip>:/var/www/jh/ dist/ 表示我想要上传当前路径下的 dist 文件夹下的所有文件 root@<your_server_ip> 这一段和前面的 ssh 与 sftp 一样,都表示用户名和对应的服务器 ip, :var/www/jh 表示文件将被上传到服务器的这个路径下。 以下是 rsync 的一些常用参数: -a:以归档模式进行同步,即保持文件的所有属性(如权限、属主、属组、时间戳等)。 -v:显示详细的同步过程。 -z:使用压缩算法进行数据传输,以减少网络带宽的占用。 --delete:在目标目录中删除源目录中不存在的文件。 -P选项是rsync命令的一个常用选项,它的作用是将--partial和--progress选项组合在一起使用。 --partial选项表示如果文件传输被中断,rsync会保留已经传输的部分文件,下次继续传输时可以从上次中断的地方继续。 --progress选项表示显示文件传输的进度信息,包括已经传输的字节数、传输速度和估计剩余时间等。 使用-P选项可以方便地同时启用这两个选项,以便在文件传输期间显示进度信息,并在中断后继续传输。 附 : 其他相关的一些操作技巧(还没写完,等我填坑) 使用 ssh-copy-id 将本地的 ssh 公钥复制到服务器上 配置 sshd 以加强服务器的安全性 sshd 是 Secure Shell Daemon 的缩写,它是一个 ssh 的守护进程,允许用户通过 SSH 协议安全地连接到远程服务器。 sshd 的配置文件应该在 /etc/ssh/sshd_config 文件中,通过更改其中一些配置项,我们可以让我们的服务器更安全。 建议 修改方式 禁用 root 用户通过 SSH 登录 在 sshd配置文件中将 PermitRootLogin 选项设置为 no。(在此之前,你应该创建一个非 root 用户并设置好对应的账号密码,修改好 /etc/sudoers 文件确保该用户能够通过 sudo 执行一些需要 root 权限去执行的语句) 强制使用 SSH 密钥登录 在 sshd 配置文件中将 PasswordAuthentication 选项设置为 no。(在此之前,你应该完成上一步 ssh-copy-id 将本地的 ssh 公钥复制到服务器上) 更改 SSH 端口 在 sshd 配置文件中将 Port 选项设置为一个未使用的端口。(在此之前,使用 ssh 命令连接到服务器时,需要使用 -p <port> 参数去指定端口) 启用 SSH 日志记录 在 sshd 配置文件中将 SyslogFacility 选项设置为 auth。 systemd 的作用与使用方法 systemd 是一个用于管理 Linux 系统的服务管理器和初始化系统。 使用 systemctl 命令管理服务状态 在我们静态网页部署这一块,我们主要用 systemctl 命令去管理一些服务的状态,比如我们想要将 caddy 设置为开机自启,这样我们即使重启了服务器,caddy 也能自动开始提供服务。 以下是一些常见的 systemctl 命令: systemctl start:启动服务。 systemctl stop:停止服务。 systemctl restart:重新启动服务。 systemctl status:查看服务的状态。 systemctl enable:使服务在启动时自动启动。 systemctl disable:使服务在启动时不自动启动。 使用 journalctl 命令来查看日志消息 在服务出现问题的时候,我们可以通过 systemctl 命令去查看服务在运行过程中留下的日志消息,方便我们去排错。 以下是一些常见的 journalctl 命令: journalctl -b: 显示当前系统日志。 journalctl -b -1: 显示最近一条系统日志。 journalctl -b -10: 显示最近 10 条系统日志。 journalctl -u <unit>: 显示指定单元的日志。 journalctl -u <unit> -b: 显示指定单元的最近系统日志。 journalctl -u <unit> -b -1: 显示指定单元的最近一条系统日志。 journalctl -u <unit> -b -10: 显示指定单元的最近 10 条系统日志。 还可以使用 journalctl 命令来导出日志消息到文件。例如,以下命令将当前系统日志导出到 /home/user/journal.log 文件: journalctl > /home/user/journal.log 防火墙的配置 关于防火墙,iptables 是 Linux 系统中最早使用的防火墙工具,它基于内核模块来过滤网络数据包。nftables 是 iptables 的继任者,与 iptables 相比,nftables 更简单易用,同时性能也更好。 但我这边想要推荐的是 ufw,他是 iptables 的一个前端,它提供一个更简单、更易于使用的命令行界面。UFW 基于 iptables 来实现其功能,但它不被用来直接使用 iptables 命令。UFW 使用自己的命令来配置防火墙,这些命令被转换为 iptables 命令并执行。 查看 ufw 状态 sudo ufw status 禁用所有端口 sudo ufw deny all 开放 22 端口(ssh 的默认端口,禁用可能导致服务器失联) sudo ufw allow 22 开放 80 端口 sudo ufw allow 80 开放 443 端口 sudo ufw allow 443 启用 ufw sudo ufw enable 包管理器是什么 Linux 常用命令 常用的一些debug手段

2023/8/4
articleCard.readMore

在运行OpenWRT的N1盒子上部署 QQBot

由于学校社团的招新需要,我写了一个依赖于 go-cqhttp 运行的 QQ Bot,并没有实现什么花里胡哨的功能,只是实现了关键词回复和新人入群时的欢迎语。因为没考虑后续维护的问题,代码也写得比较草,但毕竟是能跑。这么一个小型的程序并不会占用的多少的服务器资源,单独为这么一个 Bot 去开一台国内的 vps 似乎是有些大材小用了,刚好我手上有一台运行在 OpenWRT 上的 Phicomm N1 盒子,反正也是 Linux 系统,便打算拿来挂 QQ Bot。 安装 JDK 由于腾讯近几个月对于 Bot 风控非常严格,所以不得不采用 SignServer 项目 fuqiuluo/unidbg-fetch-qsign 来确保 Bot 账号不会被风控一次保证 Bot 运行的稳定性。而这个项目又是使用 Java 开发的,因此需要先安装 JDK/JRE。但 OpenWRT 的开发者可能并没有考虑到在路由器设备上运行 Java 程序的需求,因此 OpenWRT 的源里面是没有预先打包 JDK 的,因此我们需要额外安装。我直接 google 搜索了 install java on openwrt 的关键词,在 Github 找到了这个脚本: https://gist.github.com/simonswine/64773a80e748f36615e3251234f29d1d。但很遗憾,代码跑不起来,下载时提示 404。于是我打开脚本细细一看,脚本中 jdk 的版本号和设备的架构均需要改动。具体改动如下: - REVISION=8.212.04-r0 + REVISION=8.302.08-r1 # 版本号请自行去仓库内翻最新的 ...... - URL=http://dl-cdn.alpinelinux.org/alpine/v3.10/community/armv7/ + URL=http://dl-cdn.alpinelinux.org/alpine/v3.14/community/aarch64/ ...... - # verify packages - sha256sum -c <<EOF - e2fce9ee7348e9322c542206c3c3949e40690716d65e9f0e44dbbfca95d59d8c openjdk8-8.212.04-r0.apk - 26ad786ff1ebeeb7cd24abee10bc56211a026a2d871cf161bb309563e1fcbabc openjdk8-jre-8.212.04-r0.apk - 947d5f72ed2dc367c97d1429158913c9366f9c6ae01b7311dd8546b10ded8743 openjdk8-jre-base-- 8.212.04-r0.apk - c6a65402bf0a7051c60b45e1c6a8f4277a68a8b7e807078f20db17e0233dea8e openjdk8-jre-lib-8.212.04-r0.apk - EOF # 我这里直接将 sha256 校验给删除了,有兴趣可以自己去更新这几个文件的文件名和其对应的哈希值 随后 chmod +x 授予脚本可执行权限后直接执行,我们就将 alpine linux 上的 openjdk 成功解包并安装到了我们的 OpenWRT 中,我们只需要配置好环境变量即可完成安装。但我又比较懒,我看见 SignServer 的启动脚本里是可以通过读取 $JAVA_HOME 来获取 Java 二进制可执行文件的代码逻辑,于是我便在每次启动 SignServer 脚本前提前执行 export JAVA_HOME=/opt/java-1.8-openjdk 即可。 安装 screen 相比起前面 JDK 的安装,这一步 screen 的安装反而没有那么麻烦,在最新版本的 OpenWRT 源中,screen 已经被包括进去了,我们直接把 OpenWRT 换好源,从源里就可以安装。 opkg update opkg install screen 下载 fixed 版本的 go-cqhttp 由于 SignServer 更新,在其请求中多添加了 key 的参数要求,导致原版 go-cqhttp 的最新 release 中释出的二进制文件无法适配最新版的 SignServer,我暂时选用了一个修复了这个问题的 fork 去运行 Bot。下载到 OpenWRT 后记得也要授予可执行文件。 安装 Python 脚本中所需要使用到的库 OpenWRT 自带了 python 和 pip,这让我很欣慰。直接使用 pip 安装 flask 和 xlrd 等库即可,完全没有难度。 运行 SignServer 这一步很简单,将原项目的 Release 下载下来解压后上传到 OpenWRT 的某个路径后,开个 screen 窗口,设置好 JAVA_HOME 变量后再去调用 SignServer 中自带的 shell 脚本即可 运行 go-cqhttp 这一步也很简单,得益于 go 静态链接的特性,我们不需要为 go-cqhttp 安装任何额外的依赖就可以执行 Release 中的二进制文件,直接将我们在 PC 上登录好的 session、配置好的 device.json、config.yml 等文件上传到 N1 ,开个 screen 窗口运行即可。 运行主程序 这个没什么好讲的,同样是开个 screen 窗口运行 python main.py 的事情 python 代码如下: from flask import Flask,request import requests import xlrd # 读取 xls 中的关键词以及回应语句,将其加载到 dict 数据结构中 _data2 = xlrd.open_workbook('/root/8yue222.xls') main_table2 = _data2.sheets()[0] key_lst2 = main_table2.col_values(0)[1:] value_lst2 = main_table2.col_values(1)[1:] final_dict = dict(zip(key_lst2,value_lst2)) # 读取第二份 xls,并对相同的关键词做覆盖 _data = xlrd.open_workbook('/root/daihao.xls') main_table = _data.sheets()[0] key_lst = main_table.col_values(4)[1:] key_lst = [str(int(item)) if type(item) == float else item for item in key_lst if item != ''] key_lst.remove('Gary') value_lst = main_table.col_values(5)[1:] value_lst = [str(int(item)) if type(item) == float else item for item in value_lst if item != ''] final_dict.update(dict(zip(key_lst,value_lst))) app = Flask(__name__) class API: @staticmethod def send(message): url = "http://127.0.0.1:5700/send_msg" data = request.get_json() params = { "group_id":data['group_id'], "message":message } requests.get(url,params=params) @app.route('/', methods=["POST"]) def post_data(): data = request.get_json() print(data) if data['post_type'] == 'message': message = data['message'] messagex() elif data['post_type'] == 'notice' and data['notice_type'] == 'group_increase': welcome() else: print("忽略消息") return "OK" def messagex(): data = request.get_json() message = data['message'].replace('%','%') for key in final_dict.keys(): if key == message: API.send(final_dict[key]) break def welcome(): data = request.get_json() group_id = data['group_id'] user_id = data['user_id'] API.send("[CQ:at,qq={}] 欢迎来到浙江工业大学,精弘网络欢迎各位的到来!如果想进一步了解我们,请戳精弘首页:www.jh.zjut.edu.cn\n输入 菜单 获取精小弘机器人的菜单 哦!\n请及时修改群名片\n格式如下:姓名+专业/大类".format(user_id)) if __name__ == '__main__': app.run(host='0.0.0.0', port=5701) 参考资料: https://gist.github.com/simonswine/64773a80e748f36615e3251234f29d1d https://blog.csdn.net/qq_64126275/article/details/128586651

2023/7/31
articleCard.readMore

在浙工大宿舍使用路由器连接移动网络(校园网)

2025.09.01 Updates: 最近看到这篇博客的热度还挺高,应该是最近新生开学有不少都在看。但移动的认证方式于今年的 8 月 15 日发生了变更,具体可以看这篇微信公众号文章。我近期不在学校,没有环境去研究新的认证方式,但据我身边的人说不再需要什么 l2tp 了,找台常见的路由器直接走 dhcp 以后过一遍网页验证就好了。 祝各位新生在工大能渡过愉快的四年! 2024.04.09 Updates: 几天前移动对网页认证的页面进行了更新,原有的脚本失效,本博客已更新适配新版网页认证的脚本。 2023.7.10 Updates: 首先,搬到屏峰校区以后,l2tp 服务器确实依然为 192.168.115.1,这点挺奇怪的。 然后我发现 6.26 我的那个解决方案过于复杂,原先写的认证脚本完全可以胜任这项工作,之前失败的原因是因为我在朝晖抓的脚本参数不适用于屏峰校区,目前已经修复。脚本的变动情况可以看这里。 2023.6.26 Updates: 在我于 2023 年 6 月 26 日搬去屏峰校区以后,发生了连不上网的情况。目前一个可行的方案: 在 192.168.210.100 将自己的 MAC 地址全部解绑,然后使用自己的一台设备连接网线接口,正常通过网页验证。随后在 192.168.210.100 查看刚才通过网页验证的设备的 MAC 地址,将这一串 MAC 地址复制到 Padavan 的「外部网络(WAN)- MAC 地址」中,且将 l2tp 服务器改为 192.168.115.1 (没错,填朝晖的可以用)并重新连接 l2tp。 上一篇博客中,我为 Redmi AC2100 刷入了 Padavan,接下来就打算使用这台路由器进行联网。其实小米大多数路由器都是支持 l2tp 的协议的,只需要在路由器后台稍微设置一下就能上网,服务器 ip 填 192.168.113.1,账号密码就是 hxzha+手机尾号后8位,密码就是手机尾号后6位。我使用 Padavan 是我个人有一些别的官方固件所不能提供的功能。 l2tp 相关设置 我们将 WAN 口插上墙壁一侧的网口,左侧菜单栏点击外部网络,将外网连接类型改为 l2tp DNS 建议前两个填写学校的内网 DNS 地址( 172.16.7.10 , 172.16.7.30),最后一个填一个稳定的公共 DNS 即可,由于这一步是可选项,所以就不提供截图了。 往下拉,设置 l2tp 相关的设置项,只需要设置红色框框内的设置项即可。朝晖的 l2tp 服务器 ip 是 192.168.115.1 , 屏峰校区是 192.168.113.1 ,这里不要填错了。 网页认证脚本 做完这些步骤,其实就可以正常上网了,只不过每次断网以后可能都需要重新过一遍验证,所以我专门写了一个脚本去过这个验证。 这份脚本我已经开源到 github gist 了,在顶部填好自己网页认证时的账号密码以后就可以用了。 顶部 TODO 处要写的账号密码就是那个有图书馆背景的网页认证密码。 #!/bin/bash # Login webpage identify for China Mobile in Zhejiang University of Technology automatically # Author: zhullyb # Email: zhullyb@outlook.com # TODO: Fill Your Account and Password for 192.168.210.112/192.168.210.111 here user_account= user_password= if `ip route | grep -q 10.129.0.1`; then gateway=10.129.0.1 elif `ip route | grep -q 10.136.0.1`; then gateway=10.136.0.1 fi if whoami | grep -q "admin\|root" && [ -n "$gateway" ]; then route add -net 192.168.210.111 netmask 255.255.255.255 gw ${gateway} route add -net 192.168.210.112 netmask 255.255.255.255 gw ${gateway} route add -net 192.168.210.100 netmask 255.255.255.255 gw ${gateway} route add -net 172.16.0.0 netmask 255.255.0.0 gw ${gateway} fi # 尝试访问内网服务器,如果未通过网页认证则会获得 url 跳转信息,用于判断用户为朝晖校区或屏峰校区,并获取用户 ip test_curl=$(curl -s http://172.16.19.160) wlan_user_ip=$(echo ${test_curl} | grep -oE 'wlanuserip=[0-9\.]+' | grep -oE '[0-9\.]+') wlan_ac_ip=$(echo ${test_curl} | grep -oE 'wlanacip=[0-9\.]+' | grep -oE '[0-9\.]+') wlan_user_mac=$(echo ${test_curl} | grep -oE 'usermac=[[:xdigit:]-]+' | cut -d'=' -f2 | tr -d '-') wlan_ac_name=$(echo ${test_curl} | grep -o "wlanacname=[^&]*" | cut -d'=' -f2) # 朝晖校区宿舍楼内的移动宽带的认证请求 if echo "${test_curl}" | grep -q "192.168.210.112"; then \ curl "http://192.168.210.112:801/eportal/portal/login?callback=dr1003&login_method=1&user_account=%2C0%2C${user_account}%40cmcczhyx&user_password=${user_password}&wlan_user_ip=${wlan_user_ip}&wlan_user_ipv6=&wlan_user_mac=${wlan_user_mac}&wlan_ac_ip=${wlan_ac_ip}&wlan_ac_name=${wlan_ac_name}&jsVersion=4.2.1&terminal_type=1&lang=zh-cn&v=5099&lang=zh" # 屏峰校区宿舍楼内的移动宽带的认证请求 elif echo "${test_curl}" | grep -q "192.168.210.111"; then \ curl "http://192.168.210.111:801/eportal/portal/login?callback=dr1003&login_method=1&user_account=%2C0%2C${user_account}%40cmccpfyx&user_password=${user_password}&wlan_user_ip=${wlan_user_ip}&wlan_user_ipv6=&wlan_user_mac=${wlan_user_mac}&wlan_ac_ip=${wlan_ac_ip}&wlan_ac_name=${wlan_ac_name}&jsVersion=4.2.1&terminal_type=1&lang=zh-cn&v=5099&lang=zh" fi 在 Padavan 的设置界面中,我们去打开 ssh 服务 在自己的电脑上通过 ssh 连接到路由器的终端 ssh admin@192.168.123.1,默认密码也是 admin,就和进入 Padavan 后台的默认管理密码一样。 看了一下 Padavan 并没有自带 nano 这个方便的 tui 编辑器,只好用自带的 vi 将就一下将认证代码复制到路由器中。 vi /etc/storage/login_edu.sh 关于 vi 的使用方法我在这里也不展开讲,我个人也不熟悉这款编辑器。 将脚本复制进去后,记得输入自己网页认证的账号密码,然后保存离开,给这个脚本赋予 x 权限。 chmod a+x /etc/storage/login_edu.sh 随后运行 crontab -e ,设置运行脚本为每天早上 6 点 01 分执行一次(因为工作日凌晨 00:30 断网,早上网络恢复以后有可能会要求你通过网页认证后才能再次联网) 1 6 * * * /etc/storage/login_edu.sh 随后来到路由器的设置界面,设置「在 WAN 上行/下行启动后执行」和「在防火墙规则启动后执行」这两个地方分别调用我们的网页认证脚本,防止因停电、网线接口松动等故障恢复后依然没法联网的问题。图中的 logger 命令是给我自己排错看的,不需要设置。

2023/6/24
articleCard.readMore

为红米 Redmi AC2100 路由器刷入 Padavan

大一一年转眼就要过去了,最近要搬校区了,顺手就把之前刷过的「小米路由器4A千兆版」出手给了同学,自己反手入了一个 「Redmi AC 2100」,尽管是跟着教程走,但过程中依然是遇见了不少坑,因此就开一篇博客记录了一下。 重置路由器 这一步其实可有可无,只是我从闲鱼上入手这个路由器,买家并没有告知我密码,于是我只能手动 RESET 这个路由器来进入后台。 通过网络设置引导 原本就是连上路由器后简单地通过引导界面,但由于我没有一个正常的网络环境,所以这一步走的其实也是有点困难的,我还是稍微记一下。 首先浏览器地址栏输入 192.168.31.1 (小米家的路由器默认好像都是这个 ip 地址),看到下图界面,加不加入用户改善计划其实都是无所谓的,反正马上就要刷掉这个系统了。 此处选择「不插网线,继续配置」,因为我们没有标准的网络环境,还指望着这台路由器跑 l2tp 帮我们连校园网呢。 这里需要选择「自动获取IP」(静态 IP)好像也行,但别的选项在我的网络环境下恐怕都是没法继续配置下去的。 随后随手输个 WIFI 名称和密码,主要是记住密码进路由器后台管理。 设置完上述设置项以后,再次进入 192.168.31.1 ,就能看见路由器后台管理的登陆页面了。 获取 ssh 权限 输入登陆密码,进入路由器后台管理页面,我们需要通过 bug 去获取打开官方系统的 ssh 功能 这里提一嘴,当初我刷小米路由器4A千兆版的时候用的是这个仓库来打开 ssh。 首先是确认路由器的版本,我从闲鱼上购得的路由器自带的版本是官方稳定版 2.0.23,一开始跟着别人的思路就降级到了 2.0.7,但后来遇见问题在网上查解决方案的时候看到有人说这个漏洞在 2.0.23 依然可用,但我也没有去试。 在电脑上下载 2.0.7 的升级包,在路由器设置界面的常用设置->系统状态->手动升级 选择自己下载的升级包,确认等待重启即可。有些教程说可以选择保留数据,但我也懒得试,就直接清除了所有数据,又不得不再次过一遍上面的配置引导。 在地址栏,删除 /web/home#router 部分,加入下面这串代码 /api/misystem/set_config_iotdev?bssid=Xiaomi&user_id=longdike&ssid=-h%3B%20nvram%20set%20ssh_en%3D1%3B%20nvram%20commit%3B%20sed%20-i%20's%2Fchannel%3D.*%2Fchannel%3D%5C%22debug%5C%22%2Fg'%20%2Fetc%2Finit.d%2Fdropbear%3B%20%2Fetc%2Finit.d%2Fdropbear%20start%3B 再次删除后面的代码,加入下面这串代码 /api/misystem/set_config_iotdev?bssid=Xiaomi&user_id=longdike&ssid=-h%3B%20echo%20-e%20'admin%5Cnadmin'%20%7C%20passwd%20root%3B 两次请求的正常反馈应该长成下面这个样子。 此时应该就可以使用 ssh 访问路由器的 root 账户了,密码已经被改为了 admin ssh -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedKeyTypes=+ssh-rsa root@192.168.31.1 刷入 breed 如果用我在安卓刷机的经验来讲 breed 是什么的话,我会把他类比成第三方 Recovery (TWRP)。这是一个能够帮助你去输入系统、备份系统的恢复模式。虽然我们可以直接刷入 padavan,但如果系统没有自带镜像刷写工具或者输入的系统打不开了,那可能就是一台路由器的报废,或许得靠编程器才能救回来。 首先,我们到 breed 下载站上下载 breed 的镜像: https://breed.hackpascal.net/ 随后,在电脑上这个存放了 breed 镜像的路径上开一个 http server,我这里选择的是 darkhttpd,Windows 或者 MacOS 用户可以选择使用 miniserve,他们呢起的是一样的效果,甚至可以使用 python 直接开一个 local server。 接下来,通过自己电脑在路由器局域网内的那个 ip 地址并添加端口号在浏览器上访问你开的 http server,直接右键复制 breed 镜像的下载链接。 将 ssh 连接到的路由器终端 cd 到 /tmp 路径下,使用 wget 命令去下载你刚刚复制到的 url,这样我们就简单地将 breed 镜像传输到了路由器的内存上。再使用 mtd -r write breed-mt7621-xiaomi-r3g.bin Bootloader 刷入 breed,刷入成功后 ssh 将会自动断开连接,但并不会直接进入 breed。 我们需要先断开路由器的电源,使用一根针(比如取卡针)怼在 RESET 按钮上面,再次接通路由器的电源并持续按压 RESET 按钮几秒钟,浏览器这时就会进入 breed 状态,浏览器访问 192.168.1.1 就可以看到他的控制面板。 刷入 Padavan 在 Breed 中拥有很多的功能,不过我们用到的只是「固件更新」这一个功能,备份功能什么的可以自己尝试,这只是一个可选项。 首先去下载站下载适配 Redmi AC2100 的 Padavan 镜像: https://opt.cn2qq.com/padavan/ 然后在 Breed 的 web 端控制台直接选择 Padavan 的系统镜像进行固件更新 确认后直接刷入 自动重启后,Padavan 就刷入完成了。 Padavan 的默认 WIFI 名是 PDCN 和 PDCN_5G,WIFI 密码是 1234567890 浏览器输入 192.168.123.1 就可以进入默认的后台管理页面,管理页面的用户名和密码都是 admin 参考文章: 《小米/红米AC2100刷OpenWrt/Padavan/第三方固件的详细教程(2022年8月23日更新)》 《小米、红米 AC2100 一键开启 SSH,可自定义安装各种插件》 《解决SSH no matching host key type found 问题》

2023/6/24
articleCard.readMore

Azure 教育订阅申请时遇到的麻烦

进入大学已经快一年了,但我的 Azure 教育订阅申请一直没有成功,每年有 100 刀的额度,再这样下去就要亏掉近 700 元了,于是便打算趁期中考试刚结束的闲暇时间把 Azure 的教育订阅给过了。 我拥有 *.edu.cn 的邮箱,并且通过了 Github Student Pack 的认证,但每次在 https://signup.azure.com/studentverification?offerType=1 页面尝试申请 Azure 订阅时,总是会得到一句冷冷的「你没有资格使用 Azure 免费帐户」。于是,我找到了 Azure 订阅支持客服帮忙,链接是这个: https://azureforeducation.microsoft.com/en-us/institutions/Contact,简要填写了我的基本信息后就开始等待邮件回复了。 我是周四上午申请的,不到一个小时就等来了微软的工单生成通知 但光有这工单也没有用,我只能继续等人工客服的介入。 在周五的早上十点,我收到了来自人工客服的邮件,并且在几分钟后收到了来自人工客服的电话(电话是 021 开头的,是归属地为上海的座机打来的,但客服操着严重的港台口音,可能是港台那边的客服通过上海的座机中转打给我的?): 但很显然,这封邮件并没有提供任何行之有效的方案,我只能按照邮件中的指示将我的截图发了过去。不过客服不知道出于什么原因一定要求全屏截图(如果电话说的是全面屏,那也是指 PC 端的全屏截图),可能是他们有什么工作制度吧。回复邮件的时候一定要选择回复全部,好像是他们只使用 support@mail.support.microsoft.com 这一个邮箱与我们通信,微软的服务器收到内容后会将我们发送的内容再抄送给给我们分配的客服手上,如果不选择「回复全部」的话,客服可能就看不到之前的通信记录了。 于是就在当天下午的2点收到了人工客服的第二封邮件,说是已经帮我提交到了后台处理。 20 分钟后,我收到了这封应该是系统自动发送的邮件,说明我的账号因异常而触发了审查所以过不了 Azure 的教育订阅,需要我提交能够佐证我的学生身份的东西上去,当晚7点我便按照要求回信。 当晚8点半,我便收到了来自系统的消息,得知异常已经解除,再次申请 Azure 学生认证就成功了。

2023/5/12
articleCard.readMore

执行 repo sync 后将 git-lfs 中的资源文件 checkout

最近期中考试挺忙的,五一好不容易有一些自己的时间,于是打算重操旧业,搞点有意思的内容,没想到准备阶段就出了新问题,有点跟不上时代了 本次遇到的问题是在执行 repo sync 命令后储存在 git-lfs 中的文件没有被自动 pull 并 checkout 出来,尽管我在 repo init 阶段已经加了 --git-lfs 参数了。 上 google 简单查了查,查到一篇 stackoverflow 的回答,给出的思路是使用 repo forall -c 'git lfs pull' 的方案解决的,意思是在 repo 同步的每一个 git 仓库中都自动执行 git lfs pull 命令,但这个解决方案在我这有两个问题。 仓库的 git-lfs 没有被安装,所以 git-lfs 会直接报错 将整个安装源码一千多个仓库一一执行这些命令的速度太慢了 解决方案也很简单,直接检测每个 git 仓库下是否存在 .lfsconfg 文件,存在的话就执行 git lfs install && git lfs pull repo forall -c 'test -e .lfsconfig && git lfs install && git lfs pull'

2023/5/3
articleCard.readMore

隐式转发——骚套路建站方案

其实很久以前就接触到了国内 DNS 解析服务商提供的这个「隐式 URL」 这种 “DNS 记录类型”了,但我域名没有备案,一直没有机会去体验。 今天社团内某个同学在折腾自己博客的时候又用到了「隐式 URL」,于是就借此机会了解了一下相关内容。 DNSPOD 文档的描述如下 隐性转发:用的是 iframe 框架技术、非重定向技术,效果为浏览器地址栏输入 http://www.dnspod.cn 回车,打开网站内容是目标地址 http://cloud.tencent.com/ 的网站内容,但地址栏显示当前地址 http://www.dnspod.cn 。 也就是说,所谓「隐式 URL」,只不过是域名解析的服务商用他们的服务器去响应了访客的请求,并回应了一段使用了 iframe 的 html 。这段代码打开了一个大小为 100% 的窗口去请求了被“隐式代理”的站点。我这位同学域名是备案在阿里云下的,阿里云所使用的 html 代码如下: <!doctype html><html><frameset rows="100%"><frame src="http://example.com"><noframes><a href="http://example.com">Click here</a></noframes></frameset></html> 在下图中,我通过更改 hosts 文件实现将百度的域名在本地被解析到 localhost,并使用 iframe 标签将 b 站嵌入到页面中。当然,这并不能说明什么事情,不过是我个人的恶趣味罢了。 将 http://example.com改为目标站点,我们完全可以摆脱国内云服务商,在自己的服务器上直接实现「隐式代理」的效果。 而这种方案,恰巧可以用于在境内机子上建站,尤其是针对未备案的域名。 碍于 Github Pages 在境内的访问体验并不好,所以直接把博客部署在 Github Pages 下一直都不是首选,因此很多人都会选择去购买一台境内的小鸡,带宽虽然不大,但跑个博客什么的其实没什么大问题,但备案就很麻烦了。 我们可以通过在 Github Pages(或者其他境外的服务器) 上挂一个 index.html ,html 中使用 iframe 嵌套一个部署在境内小鸡上的网页来规避掉备案的问题。而境内小鸡可以选用非标准端口去监听请求。 这样带来的好处是访客只需要从境外的服务器上获取一个不到 1 KB 大小的 html ,随后的所有请求都是指向境内云服务器的,所以网页打开时的体验会得到改善。 隐式转发拥有以下优势: 直接向境内的云服务器发送请求,速度会得到改善 (相比于直接部署在境外服务器上的方案) 不怎么消耗境外服务器的流量 (相比于使用境外服务器反向代理的方案) 浏览器的地址栏不会直接显示 ip 或端口号(相比于未备案使用境内服务器的非标准端口的方案) 不需要备案(相比于备案后使用境内服务器的 80/443 端口的方案) 但也存在以下劣势: 移动端设备访问时好像还是会展示 PC 端的界面(存疑 现代浏览器访问时可能会有 strict-origin-when-cross-origin 的问题(一般好像是出现在 iframe 的 html 是 https 访问,而目标站点是 http 访问的情况?) 一些古老的浏览器可能不支持 iframe (? 访问目标站点的其他路径时,浏览器地址栏的显示的地址不会变 那么应某些群友的要求,本文的第二作者为 Finley,通信作者为 LanStarD。

2023/3/26
articleCard.readMore

在 vps 上配合 caddy 部署 siteproxy

之前趁着春节活动的时候在某 vps 服务商买了 1 年的 vps,线路不算太好,但勉强够用,于是打算在上面部署一些反代程序。在群友的推荐下,发现了这款名为 siteproxy 的开源项目。 siteproxy 相较于我在 r.zhullyb.top 部署的那个反代,其特点是可以运行在 vps 上,且将会替换被反代页面上的所有 url,因此遇到使用相对路径的网页也可以从容应对。 在项目的 README 中介绍了一种部署方案,但我仍有以下几点不太满意 README 中的方案仅支持 nginx 部署,但我希望使用 caddy README 中的方案使用 npm 安装了 forever 来达到保活的目的,甚至为此安装了 nvm,但我一不希望使用 npm 在系统上安装软件、二不希望安装 nvm 与 forever 原项目把根目录页做成了一个导航,指向了一些比较敏感的站点,而我希望换掉这个网页。 因此这篇博客也就应运而生。 反代 8011 端口 根据项目 README 的描述,我们应当使用 nginx 去反代 127.0.0.1:8011 端口,但我是 caddy 用户,此前也有过使用 caddy 反代的经验,所以很容易写出一段使得程序可以正确运行的 Caddyfile。 example.com { reverse_proxy 127.0.0.1:8011 { header_up Host {upstream_hostport} header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} header_up Accept-Encoding identity } } 将 example.com 的 A 记录解析到 vps 主机的 ip,并使用 systemctl 重新启动 caddy,这一步就算完成了。 安装 nodejs 我在 vps 上安装的发行版是 Archlinux,所以直接 pacman -S nodejs 安装完就是了,别的发行版应该也可以直接调用系统默认的包管理器安装 node 或者 nodejs 完成这一步。 下载程序 首先,我们需要一个地方来存放我们下载的程序,我使用的是 /opt 路径。 我们可以直接根据 README 所说的,直接 clone 整个项目,但我本人并不想这么做,项目里似乎有太多对于 vps 用户没有用的东西了。此外,整个项目首页我也不想要,首页的导航指向了一些比较敏感的网站,而我的反代就想安安心心的一个人用。 综合以上需求,我所需要的文件一共就五个: ├── config.js ├── index.js ├── logger.js ├── package.json └── Proxy.js mkdir -p /opt/siteproxy cd /opt/siteproxy wget https://raw.githubusercontent.com/netptop/siteproxy/master/{config.js,index.js,logger.js,package.json,Proxy.js} 然后补上一个 index.html 我这边选择直接使用 JavaScript 将对于 / 的访问直接重定向到我的博客。 <html><head><meta http-equiv="refresh" content="0; url=https://zhul.in/" /></head><body>Redirect to <a href="">https://zhul.in/</a></body></html> 安装依赖 在 /opt/siteproxy 目录下执行 npm install npm 将会根据 package.json 的内容自动安装所需的依赖。 修改配置文件 $EDITOR /opt/siteproxy/config.js 按照 README 所说,修改 serverName 字段 设置开机自启动 这里我选择使用 systemd 帮助我实现 siteproxy 程序的开机自启动,service 文件是我直接根据 frp 程序提供的 service 改的。 cat /usr/lib/systemd/system/siteproxy.service ----- [Unit] Description=SiteProxy After=network-online.target Wants=network-online.target [Service] Type=simple User=nobody Restart=on-failure RestartSec=5s Environment="NODE_PATH=/opt/siteproxy/" ExecStart=node --tls-min-v1.0 /opt/siteproxy/index.js [Install] WantedBy=multi-user.target 随后使用 systemctl enable siteproxy --now 启动即可访问。 为反代站点添加访问密码(可选) 参考我的另一篇博客。 使用防火墙程序禁止 8011 的公网访问(可选)

2023/2/1
articleCard.readMore

onedrive(by abraunegg) —— 一个 Linux 下的开源 OneDrive 客户端(cli)

这款 Linux 下的 OneDrive 客户端我其实一年前就已经在用了,最近打算给自己的 vps 重装系统并重新部署下 aria 的下载服务,顺便把上传到 OneDrive 的功能增加进去,便又想到了这款运行在命令行中的第三方开源 Linux 客户端,去谷歌上搜索了一番,依然没有什么成规模的中文博客去写它的用法,于是就打算自己来写。那肯定不是因为我博客这个月没有什么题材 安装 abraunegg 用 D 语言写的 OneDrive 客户端安装起来并不是什么难事,Ubuntu/Debian/Fedora 等常见发行版的仓库中均有它的身影,具体情况在 Github 项目页面中都有描述。 在 Archlinux 下,我可以直接从 AUR/ArchlinuxCN 中安装 onedrive-abraunegg 这个包来安装这个项目。 sudo pacman -S onedrive-abraunegg 运行前配置 本章内容中的所用到的和没有用到的命令都可以在该项目的 Github 仓库中找到。 在终端直接运行 onedrive 命令,程序将打印出一行地址。 使用浏览器打开地址,就会跳出微软的登陆页面,正常登陆即可。 登陆成功后,浏览器将会显示一片白屏,不必慌张,直接将浏览器地址栏中的网址复制后粘贴进终端中即可完成配置,获取到的 refresh_token 将会被保存到 $HOME/.config/onedrive 下。 账号授权成功以后我有两个迫切的需求需要在开始同步前解决: 我不希望把我 OneDrive 里所有的文件下载下来,我在 OneDrive 中存放了至少 1T 的数据,而我的系统盘就只有 512G,这绝对是放不下的,所以我想仅同步部分文件夹。 我需要修改被同步到的文件夹的路径,我不想把 OneDrive 上的文件下载到我的 /home 下。 要解决第一个需求,我们可以通过创建 sync_list 的方式指定我们要同步的文件,在 $HOME/.config/onedrive 路径下创建 sync_list ,并填入需要的文件或文件夹名,或在 !或- 后面写上不想同步的文件或文件夹名即可,支持通配符,在原仓库的文档中给出了非常详细的描述。 我们可以先使用 onedrive --display-config 命令查看我们当前的配置情况。(我这边直接应用 Github 文档中展示的内容) onedrive version = vX.Y.Z-A-bcdefghi Config path = /home/alex/.config/onedrive Config file found in config path = true Config option 'sync_dir' = /home/alex/OneDrive Config option 'enable_logging' = false ... Selective sync 'sync_list' configured = false Config option 'sync_business_shared_folders' = false Business Shared Folders configured = false Config option 'webhook_enabled' = false 这很显然,OneDrive 中的文件默认将会被保存到 $HOME/OneDrive 中。为了修改这个位置项,我们直接在 $HOME/.config/onedrive/ 路径下创建一个名为 config 的文件,把此处给的 configuration examples 全部复制进去,找到 sync_dir 把前面的注释删掉,改成自己喜欢的路径 (别问我为什么写 /tmp,问就是我内存够大 修改好此处的配置文件后,可以再次运行 onedrive --display-config 检查自己的配置文件格式有没有问题、自己更改的配置项有没有生效,这样就解决了我的第二个需求。 Standalone Mode / Monitor Mode? 这款 OneDrive 客户端支持以两种方式运行,monitor 模式将会监控本地磁盘上的文件状态,因而在同步路径内的文件从一个路径移动到另一个路径时,客户端将不会傻傻地执行「在原路径删除远端文件-重新上传新路径的本地文件」的这一个过程,具体使用 monitor 或 standalone 模式还请自行斟酌,可参考 Moving files into different folders should not cause data to delete and be re-uploaded . 开始同步 使用该客户端执行同步的命令很简单,即 onedrive --synchronize 但可选的运行参数很多,我只举出最常用的几个例子 --dry-run 使用 --dry-run 选项后,OneDrive 将不会执行同步操作,它将在终端输出原本将会被执行的操作以供你排查自己的配置是否正确。 --local-first 字面意思,--local-first 即为本地优先,同步时如果遇到文件冲突将会优先参考本地的情况。 --single-directory --single-directory 后面需要跟一个子文件夹在 OneDrive 根目录中的相对路径,这将使本次的同步操作仅对单个文件夹生效。 --download-only 字面意思,--download-only 即为仅下载模式。 --upload-only 字面意思,--upload-only即为仅上传模式,后跟 --no-remote-delete将不会在 OneDrive 网盘中删除本地相较于网盘中缺少的文件,真正做到 upload only. --resync 当下列配置项被更改时,需要执行 --resync 来确保客户端正在按照更新后的配置文件来同步你的数据 sync_dir skip_dir skip_file drive_id Modifying sync_list Modifying business_shared_folders

2022/12/24
articleCard.readMore

【翻译】关于2022年11月的事件的一些话[Z-Library]

正如我们所知道的那样,Z-Library 的主域名在前不久已经被美国警方给 take down 了,目前仅剩下 Telegram Bot 和 Tor 网络两种访问方式是我们仍然可以信任的。在11月18日,Z-Library 于其博客上发布了一篇新的文章(onion链接),此处是我的翻译版本。 As many of you know, on November 3rd most of our domains were seized and some our servers were suspended by the United States Department of Justice and Federal Bureau of Investigation. In addition, on November 16 the United States Department of Justice published the indictment against two citizens of Russia, Anton and Valeria. They are accused of criminal copyright infringement, wire fraud and money laundering to operate the Z-Library. 正如你们大多数人所知道的那样,在11月3日,我们大多数的域名和一部分服务器被美国司法部和联邦调查局封禁了。此外,在11月16日,美国司法部出发布了针对两个俄罗斯公民—— Anton 和 Valeria 的指控。他们因经营 Z-Library 而被指控犯有侵犯版权、电汇欺诈和洗钱等罪名。 We refrain commenting on the alleged Anton and Valeria involvement in the Z-Library project and the charges against them. We are very sorry they are arrested. We also regret that some authors have suffered because of Z-Library and ask for their forgiveness. We do our best to respond to all complaints about files hosted in our library if it violates author's rights. 我们不评论 Anton 和 Valeria 涉嫌参与 Z-Library 项目的行为以及对他们的指控。我们对他们的被捕感到非常抱歉,也对一些因为 Z-Library 而遭受的损失的作者表示歉意,并请求他们的原谅。如果我们的网站中托管的文件侵犯了作者的权利,我们会尽最大努力回应所有的投诉。 We see the resonance recent events caused, we see how many people support and believe in Z-Library. Thank you for your support, it is extremely valuable to us. Thank you for each donation you make. You are the ones who making the existence of the Z-Library possible. We believe the knowledge and cultural heritage of mankind should be accessible to all people around the world, regardless of their wealth, social status, nationality, citizenship, etc. This is the only purpose Z-Library is made for. 我们看到最近发生的事件所引起的共鸣,我们看到了有多少人支持并信任 Z-Library。 感谢您的支持,这对我们来说极其珍贵。我们感激您的每一笔捐款,你们是使 Z-Library 的存在成为可能的人。 我们认为,人类的知识和文化遗产应该为全世界所有人所用,无论其财富、社会地位、国籍、公民身份等,而这,正是 Z-Library 存在的唯一目的。 My makeup may be flaking But my smile still stays on 我的妆容可能会脱落 但我的笑容将会永存

2022/11/21
articleCard.readMore

【已过期】使用 vercel+supabase 免费部署 umami

讲起静态网站的访客统计,我最先使用的是百度统计,但后来转到了 umeng,发现后续的几天百度爬虫的光顾次数反而多了起来。好家伙,使用百度统计相当于把自己网站访问量向百度全盘托出,我说我的博客怎么还不被百度收录呢。 后来,umeng 推出了新的服务条款,好像是说不再向未备案的站点提供服务,随后不得不转向自部署的开源网站统计程序。 umami 提供了多种部署方式,在 vps 上可以非常轻松地使用 docker 一键部署,但上次 vps 到期时用 1Mbps 的小水管拖了好久都没有把博客前几个月的访客数据拖下来,一气之下我选择直接丢掉了这些可有可无的数据。 所以这一次,我决定放弃在自己的 vps 上部署,转去探索免费的部署方案。 umami 的官方文档上提供了非常多的部署方案,我个人比较喜欢 vercel,本站的随机图片 api 就是挂在 vercel 上的,界面比较简洁,且境内访问还算OK。 但问题在于 vercel 本身并不提供免费的数据库,所以我们不得不去寻找一些长期免费提供数据库的供应商,我选择了 supabase。 在下图中选择顶栏的 Pricing 后看到这个 $0/month 就疯狂戳烂这个 Get Started 随便填写个项目名然后输入一个足够强大的密码,地区选择美国就行,东部西部无所谓(毕竟我也不知道 vercel 的机房是在东部还是西部) 看到这个小小的绿标就说明数据库正在初始化(你先别急,让我先急 随后打开官方文档,点击其描述 vercel 那一页中大大的 Deploy 初始化过程中,vercel 会要求你创建一个 git 仓库,一般私有库就够了。 随后需要我们设置两个环境变量,第一个 DATABASE_URL 就是我们刚刚从 supabase 中复制下来并替换好 password 的 url,第二个 HASH_SALT需要你随意生成一长串字符串~~(比如你可以找一个新手让他帮你退出 vim~~ 点击 Deploy 并等上两分钟,我们就部署完啦(首页没东西,白屏是正常的 来到项目首页,点击任意域名即可访问到我们部署的 umami,不过 vercel 的域名近年来也有被污染的情况,建议在设置里绑定自己的域名。 哦对了,别忘了 umami 的默认用户名密码是admin和umami,别到时候点击进去看到登陆框一脸懵,这是在文档里写过的。

2022/11/8
articleCard.readMore

我的博客部署方案

一直以来,我的博客使用的几乎都是 Hexo 框架。 静态博客的一大优点就是可以支持 Serverless 部署,这使得我们可以直接在 Github Pages、Vercel 等平台直接部署上我的博客,如果用上 .eu.org 或者非洲国家免费域名就可以实现零成本的博客部署。 当然,我现在的博客并非是零成本搭建的,如你所见,我购入了印度国别域名 zhul.in 来凑出 竹林 的谐音。并在 Github Pages、Vercel 等平台的访问质量每况愈下的情况下又购入了位于香港的 VPS,这就引申出了今天的内容——介绍我博客的部署方案。 我的博客是使用 HK vps + Github Pages 两处部署实现的,通过 dnspod 免费版的域名分境内/外解析实现了分流。当境内的访客访问我的博客时,他们将会被解析到香港的 vps 上以获得更好的体验,而境外的访客将会被解析到 Github pages,毕竟 Github Pages 在境外的速度并不慢,并且稳定性肯定比我这小鸡要好得多。 不过关于通过 dns 解析分流这件事,之前看城南旧事的博客中有提到可以使用境外的 GeoScaling 完成,其免费支持全球分as、城市、经纬等智能解析,也支持自编辑脚本,看起来以后可以去试一试。 而 Hexo 框架最被人诟病的一点是更新麻烦。这一点不可否认,使用 hexo generate 生成静态网页文件再部署到服务器上的过程在一台新设备上是不小的工作量,它涉及 git、nodejs 的安装,涉及到 ssh key 和 rsync,整个环境的搭建就要废上不小的工夫。 在博客内容的更新方面,我选择了将整个 Hexo 的 workdir 全部上传到 github,使用 Github Action 帮助我同时完成静态页面的生成和 Github Pages 及 vps 的部署工作。具体的代码可以直接见我的 GIthub 仓库,我在这里简单讲下思路。 安装 nodejs 这个没什么可说的,有现成的 Github Action 去完成这件事,我这边直接使用了actions/setup-node@v2。 使用 npm/yarn 安装相关依赖 这个直接跑 yarn install 即可。 为每个文件重新设定最后修改时间 这一步其实是挺重要的,Hexo框架生成每篇文章的最后修改时间的依据是该文件的最后修改时间,而对于 Github Action 的容器来说,每一个文件都刚刚被下载下来,都是最新的,这就会导致你的每一篇文章每次部署时都会被认为刚才修改过。 我们这边可以直接使用 git 记录的时间来作为文件的最后修改时间。(参考 Sea's Blog) git ls-files | while read filepath; do touch -d "$(git log -1 --format='@%ct' $filepath)" "$filepath" && echo "Fixed: $filepath"; done 设置时区 读我的博客的人应该大多都是东八区的人,那我们应当把 Github Action 容器的时区设置为东八区,和自己 git commit 时所使用的设备的时间保持一致,否则某些文章的日期可能会发生一天的偏移。 export TZ='Asia/Shanghai' 生成静态网页文件 yarn build 部署到 Github Pages 使用 peaceiris/actions-gh-pages@v3 初始化 Github Action 容器上的 ssh 私钥 应当在 Github 仓库的设置里先新建一个 secret,填入自己的 ssh 私钥(更加标准的做法应当是为 github action 专门生成一对 ssh 密钥,将公钥上传到自己的 vps,将私钥上传到 Github 仓库的 secret 中)。 我这边直接从点墨阁那边抄了点代码直接用。 使用 hexo 的 deploy 插件调用 rsync 将静态文件上传到自己服务器的对应目录(static server 你应当已经设置好了) yarn deploy 注: 本篇博客中引用的所有博客页面均在 web.archive.org 进行了存档,如后续遇到页面打不开的问题请自行前往查询存档。

2022/11/4
articleCard.readMore

使用 VirtScreen 将 Pad 作为副屏

由于浙江工地大专的朝晖尚9宿舍实在是太小了,我没有办法放下一块便携显示屏,所以只能把家中闲置的 Huawei Pad M6 作为自己的副屏。 经过一轮搜索下来,我找到了 VirtScreen 作为工具。 安装 在 Archlinux 上,大概有三种以上的方式进行安装: 一、使用 AUR 上的 virtscreen 遇到的唯一一个麻烦是作为依赖之一的 python-quamash 在 python3.10 上无法直接安装。通过 AUR 的评论区得知,需要将 collections.Mapping 改为 collections.abc.Mapping 方可通过安装。 二、使用 dderjoel 的 fork 进行安装 见 https://github.com/dderjoel/VirtScreen/blob/master/package/archlinux/PKGBUILD 三、直接通过 appimage 安装,不过需要自己手动安装 x11vnc 配置 系统层 打开软件以后,我们需要先在 Display->Virtual Display->Advaced 选择 VIRTUAL1 作为显示屏。 如果没有这个选项,可能需要根据自己的显卡做出相应的调整。 可以参考 ArchWiki。 软件层 在这里,我们需要根据我们作为副屏的设备的屏幕分辨率来计算我们需要在 VirtScreen 中设置的分辨率参数。 我的 Huawei Pad M6 是 2560*1600 的分辨率,但 VirtScreen 最高支持只有 1920*1080,所以我们需要选择 1280*800,并开启高分辨率选项。 VNC 那边只需要根据自己的需求设置一下密码即可。 使用 在 VirtScreen 的 Display 界面点击 "Enable Virtual Screen",切换到 VNC 界面点击 "Start VNC Server",可以勾选右侧的 "Auto"。 Pad 端只需要安装任意一个 VNC 客户端即可,我这里使用的是"VNC Viewer"。 图片

2022/10/4
articleCard.readMore

在 Archlinux 下使用 l2tp 协议连接校园网

由于高考爆炸,所以不得不进入浙江工地大专来度过自己接下来四年的人生(希望到时候可以借助学校的力量润出去)。 学校这边由于某些不可描述的原因,将校园卡与宽带捆绑销售,且每次登陆校园网时都需要使用定制的 l2tp 协议客户端进行上网,且该客户端将会禁用用户的无线网卡(这不明摆着想让我们宿舍每个人都花一次钱)。 更惨的是,学校仅提供了 Windows 与 MacOS 的客户端。 在 BearChild 的提醒下,我意识到 Linux 下也可以有 l2tp 协议。 谷歌搜索了一番,我在简书上捞到了这篇文章《ubuntu 连校园网 via l2tp》。不过这显然有些麻烦,我们的客户端不需要 pppoe 拨号,只需要插上网线后连接 l2tp 协议即可联网。 所幸,NetworkManager 非常贴心地为我们提供了 l2tp 的插件,在 Archlinux 下使用如下命令即可完成安装。 sudo pacman -S networkmanager-l2tp 安装完成后,就可以在图形化界面下进行我们的设置操作。 由于定制的客户端已经把 l2tp 服务器 ip 写死且显示在界面上了,我们就不需要再去抓包截取服务器 ip,直接使用这边的 192.168.115.1 即可。

2022/9/29
articleCard.readMore

为 Element 添加自己喜欢的贴纸

在读这篇文章之前,你应该已经知道 element、matrix 是什么,这部分内容咱就不过多展开讲了。 需要准备的 PC 端 element python3.6+ 环境 能够挂静态资源的站点(比如 Github Pages、Gitlab Pages、Vercel 等免费平台的账号) 可能需要能够突破大局域网限制的网络环境 需要用到的项目 maunium/stickerpicker 克隆主项目 git clone https://github.com/maunium/stickerpicker.git && cd stickerpicker 使用 pip 安装依赖 其实本来想直接用包管理去安装这个项目的依赖的,可惜我看了一眼依赖列表,有整整一半的依赖没有被 Fedora 打包,所以干脆就直接用 pip 安装算了。 pip install . 选择一:将本地图片制成贴纸包 在项目根目录下创新一个新的目录。 mkdir <pack directory> 将需要的图片放入其中。如果需要排序,可以在图片的文件名最前面加上数字标号。 执行命令进行打包 sticker-pack <pack directory> --add-to-index web/packs/ 如果想要给目录贴纸包命名,则可以追加--title <custom title>,否则将直接设置为目录名 选择二:从 tg 获取现成的贴纸包 项目内已经为我们准备了 sticker-import 命令来帮助我们直接从 tg 获取表情包,那我们直接收下 sticker-import <pack_url> 第一次使用时,会要求我们登陆 matrix 和 tg 账号 matrix 的 homeserver 和 access token 可以在 PC 端 element 的设置里找到 tg 登陆时需要你输入手机号码,或者某一个 tg bot 的 token,这个大家都懂。 运行完成后,贴纸包就被上传到了你所使用的 matrix homeserver 上。 接着我们需要做的事情就是将 web 文件夹部署到 github pages 等做成静态站点,这个比较简单,不再赘述,我这里直接部署在了 https://matrix-sticker.zhullyb.top 我们下文就直接拿它做演示,看得上的也可以直接拿来用。另外,@朝色 的 url 也可以直接拿来使用 https://sticker.zhaose.cyou/web/ 添加到 element 这是本篇文章最吊诡的地方,element 其实并没有为我们准备这么一个添加自定义 sticker 的地方,从某种意义上讲,我们是把我们的 sticker 给 hack 进去。 在 element 的 pc 端找到任意一个对话框,输入 /devtools 并发送 将会出现如下页面,选择 Explore account data 找到 m.widgets,如果没有,就点击下图标出的按钮 在新的页面中,填写如下内容,url 那一行应当改为自己部署的页面,并发送请求 { "stickerpicker": { "content": { "type": "m.stickerpicker", "url": "https://matrix-sticker.zhullyb.top/?theme=$theme", "name": "Stickerpicker", "data": {} }, "sender": "@you:matrix.server.name", "state_key": "stickerpicker", "type": "m.widget", "id": "stickerpicker" } } 重启 element,此时就可以享受到自己导入的 sticker 了,手机端的 element 设置也将会被同步。 补一张效果图

2022/8/10
articleCard.readMore

nodejs16:是我配不上 openssl 3 咯?

今年上半年升级 Fedora 36 的时候遇到了这个问题。 那会儿很无奈,一直在等 nodejs16 合并提供 --openssl-legacy-provider 的那个 PR。nodejs16 是一个 lts 版本,照道理来说,既然要提供 Long-term Support,而 openssl 1 作为它的依赖之一,生命周期结束又在 nodejs16 之前,那是不是应该给 nodejs16 backport 在 nodejs17 上实现的 --openssl-legacy-provider 参数选项呢?否则绝大多数发行版都会在 openssl 1 的生命周期结束之前切换到 openssl 3,那 nodejs16 不就没法用了嘛。 然而,nodejs 在他们的官网上发布的一篇博客刷新了我的世界观,而此前的那个 PR 甚至一度被关停。(此处有寒晶雪提供的中文翻译) 博客称他们将会把 nodejs16 的生命周期结束时间提前以防止 openssl 1 生命周期在 nodejs16 生命周期结束之前结束(这种做法甚至还有先例) 很无奈,那会儿有两个 npm 管理的软件没法在 Fedora 36 上编译出来,就一直搁置了下去。 不过好在,事情还是有转机的。(要不然就这档子鸡毛蒜皮的小事我也不会专门去写篇博客出来) 前几天我给 atpoossfl 仓库打了 rpm 版本的 nvm 以后,意外地发现 nvm 所提供的 nodejs 会自带 openssl。 所以我们只需要使用 nvm 安装的 nodejs16 即可解决 Fedora36 以后没有 openssl 1 的问题。 使用 Fedora 的用户需要注意,Fedora 官方源中的yarnpkg在打包时遇到了错误,他们将 /usr/lib/node_modules/yarn/bin/yarn.js 的 shebang 给改成了 #!/usr/bin/node,应当改回 #!/usr/bin/env node才能让 yarn 正常使用上 nvm 提供的 nodejs;或者干脆添加 dl.yarnpkg.com 提供的 yarn 软件包。在写 specfile 的 BuildRequires 时,可以直接写成 /usr/bin/yarn 来避免频繁在 yarn 和 yarnpkg 这两个包名间改动。 更好的消息是,nodejs 已经在 v16.17.0-proposal 和 v16.x-staging 分支收下了这个为 nodejs16 提供 --openssl-legacy-provider 的 commit。相信在不久的将来,这个 commit 将会进入主线,并在 v16.17 版本的 nodejs16 上发挥它的作用。

2022/8/4
articleCard.readMore

如何拯救失声的 hollywood

我刚开始接触 Linux 下的 hollywood 时,我记得它运行时是有声音的,应该是 007 的主题音乐,如今再次装上 hollywood,却发现音乐没了。 在 Github 找到 hollywood,发现有一个 issue 也提到了这个问题。 原作者在该 issue 中回复道 没错,它只是一段视频,音频受到版权保护。 所以不难看出,作者因为版权问题而去掉了音频,进而导致 hollywood 失声。但我们作为用户,是不是可以想办法获取到老版本中那段带有音频的 mp4 文件呢? 答案是肯定的。 得益于 git 的版本控制特色,在 hollywood 的 github 仓库中,我们可以找到原来的 mp4 文件。 下载这个 mp4 文件后,我们将其放入 /usr/share/hollywood/ 路径下,重命名为 soundwave.mp4,并确保其被正确设定为 0644 权限。 sudo install -Dm644 ./mi.mp4 /usr/share/hollywood/soundwave.mp4 接下来试着跑一跑 hollywood,发现依然没有声音。再次查阅源码,发现缺少了 mplayer 这个依赖。 使用包管理器安装 mplayer 后,运行 hollywood 就可以听到声音了。 然而,你觉不觉得这个音乐。。。听上去怪怪的。。。 没错,作者在去掉音频后,给 soundwave.mp4 设定了加速播放。而我们现在需要这段视频被原速播放。编辑 /usr/lib/hollywood/mplayer #!/bin/bash # # Copyright 2014 Dustin Kirkland <dustin.kirkland@gmail.com> # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. command -v mplayer >/dev/null 2>&1 || exit 1 trap "pkill -f -9 lib/hollywood/ >/dev/null 2>&1; exit" INT PKG=hollywood dir="$(dirname $0)/../../share/$PKG" -DISPLAY= mplayer -vo caca -loop 0 -ss $((RANDOM % 100)) -speed 100 $MPLAYER_OPTS $dir/soundwave.mp4 +DISPLAY= mplayer -vo caca -loop 0 $MPLAYER_OPTS $dir/soundwave.mp4 再次运行,确认修改已经成功。

2022/7/25
articleCard.readMore

处理 fcitx5 的文字候选框在 tg 客户端上闪烁的问题

文章开头,先要感谢 fcitx5 的开发者 老K 帮我 debug 这个问题 鬼畜的文字候选框 在新装的 Fedora 36 KDE Wayland 下使用 fcitx5 时遇到了文字候选框前后移动晃眼的问题(如下图) 解决方案 当我向老K提出这个问题上的时候,老K告诉我这是预期行为,一共有两个解决方案。 使用 qt 的 text input 关掉 kwin 的淡入淡出特效 但由于我并不熟悉 KWin 的特效,所以我选择了前者的方案。 首先,需要确保自己的 Plasma 版本在 5.24 或以上,fcitx5 的版本号在 5.0.14 以上。 然后我们需要让 KWin 去启动 fcitx5。KCM 为此提供了一个非常简单的方式,如下图 随后需要确保环境变量没有设置 QT_IM_MODULE 。一定要确保这个变量不存在,连空也不行,必须是 unset。 理论上来说,是不需要重启的,但我的环境变量是 fcitx5-autostart 这个 rpm 包在 /etc/profile.d/fcitx5.sh里面设置的,我需要重启系统来使新的环境变量生效。 重启后,如果没有什么意外的话,就算成功了。 绝对不会缺席的意外 很遗憾,我遇到了意外。 完成上述操作后,文字候选框依然有问题。 在老K的正确推测下,是因为我在 Fedora 下曾经使用过 im-settings,该程序在 $HOME/.config/environment.d/ 路径下重新帮我设置回了 QT_IM_MODULE 这个变量,从而使得 tg 启动时还在使用 IM MODULE,而不是预期的 qt text input。 删除这两个影响环境变量的文件后,在 tg 输入时,fcitx5 的文字候选框恢复了正常。 debug 过程中用到的两个方式 dbus-send dbus-send --print-reply=literal --dest=org.fcitx.Fcitx5 /controller org.fcitx.Fcitx.Controller1.DebugInfo 运行如上命令后,我得到了如下的输出 Group [x11::1] has 0 InputContext(s) Group [wayland:] has 5 InputContext(s) IC [a50fe208d42e4611b240c0b66a2fa0b9] program:konsole frontend:dbus cap:e001800060 focus:1 IC [d7d4d5c05e9c445aab1af9c7dfb5fbd4] program:telegram-desktop frontend:dbus cap:e001800060 focus:0 IC [ac72ec3edf58481bbdf838352520efd5] program:krunner frontend:dbus cap:e001820060 focus:0 IC [d8b450176e204953837248f786204c29] program:plasmashell frontend:dbus cap:e001800060 focus:0 IC [df252979343d42ebbe9bd82ead6ff194] program: frontend:wayland cap:40 focus:0 Input Context without group 老K指出,出现了 telegram 的那一行表明 tg 还是在用 IM Module,所以是环境变量有问题 /proc 查看程序运行时的环境变量 参考资料 Use Plasma 5.24 to type in Alacritty (Or any other text-input-v3 client) with Fcitx 5 on Wayland Candidate window is blinking under wayland with Fcitx 5 查看进程的环境变量 注: 上述参考资料均已在 web.archive.org 和 archive.ph 做过存档,如遇到原站点无法访问的情况,可自行前往这两个站点查看存档。

2022/7/3
articleCard.readMore

使用caddy反向代理维基百科中文站点

反代的目的无非是两点 满足自己在无代理情况下访问无法访问的站点的需求 方便将站点分享给亲朋好友。 一直以来,我都想用 caddy 去反代一份维基百科来用,今天刚好就顺手解决了。 注意事项 用于反代的机子需要有对目标站点的访问能力 最好准备一个新的域名作为白手套,防止被污染 建议增加密码保护,一来使得小鸡流量不被滥用,二来防止防火墙检测到站点内容 本文使用的 caddy 开启了 replace_response 插件,可以使用 xcaddy 编译或直接前往 https://caddyserver.com/download 勾选相应插件后下载。安装时,建议先根据官方文档安装原版 caddy,再用启用了 replace_response 插件的 caddy 二进制文件覆盖掉原版 caddy,这样就不需要去手写 systemd 相关的文件了。 Caddyfile { order replace after encode } https://zhwiki.example.com { reverse_proxy * https://zh.wikipedia.org { header_up Host {upstream_hostport} header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} header_up Accept-Encoding identity header_down location (https://zh.wikipedia.org/)(.*) https://zhwiki.example.com/$2 header_down location (https://zh.m.wikipedia.org/)(.*) http://m.zhwiki.example.com/$2 } replace { "upload.wikimedia.org" "up.zhwiki.example.com" "zh.wikipedia.org" "zhwiki.example.com" "zh.m.wikipedia.org" "m.zhwiki.example.com" } } https://m.zhwiki.example.com { reverse_proxy * https://zh.m.wikipedia.org { header_up Host {upstream_hostport} header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} header_up Accept-Encoding identity header_down location (https://zh.wikipedia.org/)(.*) https://zhwiki.example.com/$2 header_down location (https://zh.m.wikipedia.org/)(.*) http://m.zhwiki.example.com/$2 } replace { "upload.wikimedia.org" "up.zhwiki.example.com" "zh.wikipedia.org" "zhwiki.example.com" "zh.m.wikipedia.org" "m.zhwiki.example.com" } } https://up.zhwiki.example.com { reverse_proxy * https://upload.wikimedia.org { header_up Host {upstream_hostport} header_up X-Real-IP {http.request.remote.host} header_up X-Forwarded-For {http.request.remote.host} header_up X-Forwarded-Port {http.request.port} header_up X-Forwarded-Proto {http.request.scheme} header_up Accept-Encoding identity } } 简单解释 第一大段是启用 replace_response 插件的部分,直接照抄即可。 第二和第三大段的思路是一致的,分别反向代理了 PC 端和移动段的网页。两行 header_down 的写法是受到了知乎上那篇 Github 反代的启发,避免了源站发出 302 重定向时访客被带到源站去。replace 部分不用多说,就是将针对三个源站域名的请求改到反代站域名。 第四大段就是中规中举地反代了 upload.wikimedia.org 这个域名,上面存放的大多数是媒体文件,如果条件允许的话其实可以考虑使用多个服务器反代。 密码保护在我这份 Caddyfile 中没有启用,如果有需要的话可以参考我的另一篇博客。 参考资料 Caddy 官方文档 The Road to Serfdom——如何为GitHub搭建反向代理 使用 Caddy 配置 Wikipedia 反向代理 使用 Caddy 反代 ghcr.io 所有参考资料除官方文档外均使用 web.archive.org 和 archive.ph 进行存档,如有无法访问的情况,请自行前往存档站获取历史存档。

2022/5/30
articleCard.readMore

创建一个本地的 Fedora 镜像源

Fedora 36 在多次跳票后,总算是在 5月10日正式发布了。截止北京时间 5月11日凌晨两点,上海交通大学开源镜像站的上游 rsync://download-ib01.fedoraproject.org/ 仍然没有同步 Fedora 36 的 Release 源。鉴于 Release 自 freeze 以后基本是不会有什么大变动的,也不需要及时同步更新,干脆就直接建立一个本地的镜像源。 准备 一块足够大的硬盘 根据我个人实测,单 Fedora 36 的 x86_64 架构 的 Release 源中的 binary rpm 就占用了 89.6 GB,具体准备多的的硬盘空间还得看你具体需要同步些什么。 符合要求的上游 这里所说的符合要求一共是两个方面,一是允许 rsync 同步,二是有你想要的文件。我通过 getfedora.org 的下载按钮的转发目标得知 mirror.karneval.cz 已经完成了 Fedora 36 Release 源的同步。 良好的网络条件 这里说的良好的网络条件,并不一定是说需要访问境外站点的能力,而是你和你的上游之间的网络访问畅通,不要动不动就i断开连接那种。如果你选择的是国内镜像站作为你的上游,那一般不会有什么问题。 开始同步 现在的主流方案一般都是选择 rsync 直接开整。 试探环节 很多镜像站的 rsync 文件路径和 http 文件路径路径是不同的。 比如说,我这里用的 mirror.karneval.cz 的 http 页面显示的 fedora 仓库路径在 /pub/fedora,但 rsync 同步时需要使用 /fedora 路径。 为了确定这一点,我们可以先通过 rsync rsync://example.com 进行预览 rsync rsync://mirror.karneval.cz 通过一层一层预览目录的方式,找到需要同步的路径是 /fedora/linux/releases/36/Everything/x86_64/os/ 同步环节 通过 mkdir 和 cd 创建并进入我们准备用于同步源码的文件夹,然后开始执行同步命令。 rsync -avP rsync://mirror.karneval.cz/fedora/linux/releases/36/Everything/x86_64/os/ . Ps: 中途如果由于各种原因而中断了同步过程,可以再次使用上述命令继续同步,rsync 会保证文件完整性。 安装、配置并启用 static server (可选) 如果只需要本机使用,那么直接跳过这一步即可;如果需要给局域网内的其他机器提供镜像源,那么需要启用 static server。 我这里选择的是 caddy,性能虽然比 nginx 略逊一筹,但胜在配置简单。 caddy 的安装可以直接参考官方文档,这里不再赘述。 配置也不过那么几行的事情,我给个 example。端口号只要和别的程序没有冲突,就可以随意指定。443 端口需要 ssl 证书比较麻烦,局域网内直接用非标准端口即可。 :14567 { root * /the/directory/you/use file_server { browse } } 配置完后直接以普通用户的权限启用即可,使用 systemd 启用需要解决 caddy 用户对目标无权限的问题。 caddy run --config /etc/caddy/Caddyfile 浏览器输入对应的 ip 和端口,应该就可以访问了。 修改源配置文件 由于我们仅同步了 Release 源,就只需要修改 /etc/yum.repo.d/fedora.repo 即可。 如果镜像源在本机上,可以直接使用 file:// 协议头: [fedora] name=Fedora $releasever - $basearch + baseurl=file:///the/directory/you/use - metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch enabled=1 countme=1 metadata_expire=7d repo_gpgcheck=0 type=rpm gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch skip_if_unavailable=False 如果镜像源在同局域网设备上,通过 http:// 协议也能达到相同的效果: [fedora] name=Fedora $releasever - $basearch + baseurl=http://192.168.1.233:14567 - metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch enabled=1 countme=1 metadata_expire=7d repo_gpgcheck=0 type=rpm gpgcheck=1 gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch skip_if_unavailable=False Ps: 提供镜像源的机子的局域网 ip 可以通过 ip -br a 命令获取

2022/5/11
articleCard.readMore

好软推荐——FastOCR

前两天在 PC 端有 OCR 的需求,需求如下 自带框选功能或者图片上传前的编辑功能 硬盘占用小,不要 electron (((已经受够了 支持系统托盘或者快捷键快速调出 免费 在李皓奇的推荐下试用了 Arch 群兔兔拿 python 和 qt 写的 fastocr,体验可以说是相当不错了。四个要求基本都能完美满足! 支持 百度、有道、旷视Face++ 三家的接口,免费额度绝对够我试用的(大不了一家用完了换一家嘛 此外,空间占用小,算上依赖也不过 31MB 的硬盘空间占用,连半个 electron 都不到,运行起来反而更加流畅 <^_^>

2022/4/14
articleCard.readMore

抛弃PicGo,直接使用curl将图片上传到LskyPro

前一阵子为了图床折腾了好长一段时间。刚开始用的是 cloudinary,虽然每月有限制,但强在境内访问速度还不错,可惜后来 res.cloudinary.com 这个域名在某些地方被 DNS 污染了,而自定义域名是付费版的功能,就不得不放弃了。 后来也尝试过 npm图床 的方案,可惜面对这种滥用公共资源的行为我无法接受~~(肯定不是因为受不了那繁琐的上传步骤,随便传张图都得 bump 下版本号的原因)~~,而且现在境内的能作为图床使用的 npm 镜像似乎也就只剩下 npm.elemecdn.com 这一个能够正常回源了,没准哪天就用不了了,所以就去投奔了杜老师的去不图床。 去不图床采用开源图床程序 Lsky Pro 搭建,没有免费服务,且配置了鉴黄服务,看起来就是打算长久做下去的图床站点。境内使用腾讯云cdn,境外采用 cloudflare cdn,速度都挺让我满意的。(杜老师看见请给我打钱,或者多送我点空间也行(x Typora 一直是我写博客的主用 Markdown 编辑器,之前我采用 Typora 调用 PicUploader(php) 自动上传图片的方案写博客,体验相当不错,如图: 可惜 PicUploader 目前仍然没有支持 LskyPro 的上传,我采用的是现在烂大街的 Typora+PicGo+LskyPro插件 的方案去实现 Typora 的自动上传图片功能。 这个方案有明显的弊端: PicGo 运行依赖于 electron,极大地消耗了系统资源。 PicGo 面对多张图片( >=4张 )同时上传时容易报错。 PicGo 对于 Linux 的支持比较有限,作者可能不熟悉 Linux,直到半个月前我去交了一个 pr 才支持 wayland 下使用 wl-clipboard 将图片链接复制到粘贴版。 正好 LskyPro 有详细的文档,应该可以用 curl 手糊一段 Shell 脚本实现直接上传,资源占用小,唯一的弊端是上传完成后的图片不容易管理。脚本如下 #!/bin/bash API_URL="https://7bu.top/api/v1" AUTH_TOKEN="" markdown=false while [[ "$#" -gt 0 ]]; do case $1 in -m|--markdown) markdown=true; shift ;; *) break ;; esac done for images in "$@"; do UPLOAD_RESPONSE=$(curl -s -X POST "$API_URL/upload" \ -H 'Content-Type: multipart/form-data' \ -H "Authorization: Bearer $AUTH_TOKEN" \ -F "file=@$images") if [ "$markdown" = true ]; then echo "$UPLOAD_RESPONSE" | jq -r .data.links.markdown else echo "$UPLOAD_RESPONSE" | jq -r .data.links.url fi done 2022/04/02更新: 第六行 $@ - "$@",解决文件名中出现空格时导致的上传失败问题。 需要借助 jq 来读取返回的 json,各 Linux 发行版源内应该都有打包,自行安装即可。 授予x可执行权限后,Typora 内直接填写自定义命令输入脚本所在位置即可实现 Typora 自动上传图片了。 2023/12/12更新: 支持 -m 或 --markdown 参数,使脚本输出 markdown 格式的链接。

2022/3/31
articleCard.readMore

使用 Github Action 跑 rpmbuild

一直打算用 Github Action 跑 rpmbuild 构建 rpm 包,然后传到 Action 的 Artifacts 里面,用户就可以在登陆 Github 帐号的情况下进行下载。只要不发 Release,应该就不算「再分发」的行为,也就自然规避了再分发闭源软件的法律风险。 然而,现有的那些 Action 几乎全都是针对 CentOS 老古董定制的,,有些甚至连 buildrequires 都不帮你安装,而且大部分情况下都不支持 Source 直接填写一个链接,需要你直接提供 Source 文件。我自己又不可能在 Github 的仓库里用 lfs 强行存一个 200MB+ 的二进制文件,显然是不符合我要求的。还有几个项目使用 mock 去构建的,但使用 mock 构建需要提前用 rpmbuild 生成 srpm,在我们的个人电脑上可以理解为用一个干净的 chroot 打包防止自己的环境受污染,但在一个全新的、用完一次就要扔掉的 docker 里面还要防止环境被污染似乎有些画蛇添足的嫌疑。 最终,我选择了 naveenrajm7/rpmbuild 这个项目。(虽然我并不理解为什么他要用 nodejs 去调用系统命令去执行 rpmbuild 等一系列步骤,我也没学过这类语言。不过项目的 main.ts 我还是能仿写的。) 在经过三四个小时的摸爬滚打下,我还是成功地将这个项目按照我的想法改完了。 采用 Fedora 35 作为 host 进行 rpmbuild 自动安装 buildrequires 自动下载 source 允许仓库内自带本地 source 移除针对 srpm 的构建 改完后的 action 在 zhullyb/rpmbuild-github-action,欢迎使用。 最终是在 zhullyb/dingtalk-for-fedora 项目成功实装了,有兴趣的访客们可以去尝试着一起来白嫖 Github Action 呀! >_<

2022/3/6
articleCard.readMore

如何打出一个「-git」的rpm包

本文中,笔者通过 github api 获取最新的 commit_id ,以一种曲线救国的方式成功为 rpm 打下了一个 -git 包。 On Archlinux 用过 AUR 的 Arch 用户应该知道,makepkg 支持 "-git" 包。当我们执行 makepkg 时,PKGBUILD 中的 pkgver 函数会自动被运行,并将输出的结果作为本次打包的版本号。这是一个非常棒的设计,我们不需要去手动更新 PKGBUILD,就可以直接从 git 服务区拉取最新的 master 分支编译打包,对于跟进开发进度而言非常方便。 一般来说,一个 -git 包的版本号会分成 2~4 个部分,最为核心的是 count 和 commit_id:count用于记录这是第几次提交,通过提交的次数作为版本号的靠前部分可以帮助包管理器比较版本号的新旧,比如第21次提交的代码一定比第18次的更加新,而21也正好比18大,包管理器也就凭借着这个数字来保证其可以在用户在更新的时候为用户选择一个更新版本的包;而 commit_id则可以帮助人类更快定位这个包是在哪一次代码提交以后编译的,以帮助 开发者/用户 定位问题。 On Fedora 然而,这个思路在 rpm 上似乎无法实现。rpmbuild 执行的时候会事先根据版本号在 BUILDROOT 路径下创造一个 %{name}-%{version}-%{release}-%{arch}的目录,如此一来,就必须先确定版本号,无法像 PKGBUILD 那样使用一个 pkgver 的函数去自动更新版本号。此外,rpm 似乎专注于软件包的 Reproducibility,也就是希望拿到了指导 rpmbuild 打包的 specfile 以后打出一个相同包的能力,因此,使用同一份 specfile 在不同时间打出一个不同包的这种行为似乎并不符合 Fedora/Redhat 的哲学,所以我们怕是等不到 rpm 支持这个功能的那一天了。 Turn of events 当然,这也并非不可能完成的任务,在 西木野羰基 的指引下,我在 Fedora Docs 找到了对于某个 Branch 的打包样版。其实也就是直接从 github 下载 master 分支的 master.tar.gz 压缩包来获取最新的源码,这样就确保了每一次 rpmbuild 的时候都能获取最新的源码。接下来需要处理的就是版本号的问题。 Sad Story 很可惜,master.tar.gz 压缩包中并不包括 .git 文件,我们无法通过 git rev-list --count HEAD 来获取 count 计数,此外,最新的 commit_id 我们也不得而知。即使我们知道这些参数,也无法在 rpmbuild 执行之前自动把这些参数填进 specfile 中。 Improvement 好在天无绝人之路,在 Liu Sen 的 RPM 中宏的简单介绍 一文中发现宏其实也可以类似 bash 中的 $() 一样定义成系统运行某些命令后的结果,通过仿写 copr 上 atim/fractal 的 specfile 定义了下面两个宏。 %global timenow %(echo $(date +%Y%m%d.%H%M)) %global commit_short_id %(api_result=$(curl -s https://api.github.com/repos/<username>/<reponame>/branches/master | head -n 4 | tail -n 1); echo ${api_result:12:7}) 版本号就可以直接写成 %{timenow}.%{commit_id_short} %{timenow} 是直接通过运行系统的 date 命令获得一个精确到分钟的时间来当作 count 给 dnf 判断版本号大小使用 %{commit_id_short} 从 api.github.com 获取到该仓库最新的 commit 号,配合粗制滥造的 shell 命令做切片,提取前7 位,帮助用户和开发者快速定位源码版本使用。当然,也可以选择直接使用 jq 作为 json 的解释器,不过 copr 大概率没有预装,生成 srpm 的时候估计就会报错。 Review 至此,我们成功解决了在 rpm 上打 -git 包的问题,不过仍然有以下缺点 仅支持 github 上的项目,对于其他的 git 托管服务商还需要去查阅他们的 api 文档 粗制滥造的 shell 命令可能不足以应对以后的 github api 变更 使用了精确到分钟的时间作为计数器,导致版本号过长 使用 copr 打包的时候,有概率出现 srpm 与 rpm 之间版本号出现分钟级的差异

2022/2/7
articleCard.readMore

雪藏在开源镜像站点中的那些常用却不为人知的软件

前两天在下载 微PE 的时候眼睛突然一瞥,发现了山东大学的开源镜像站。突然间才发现在各个开源镜像站点中提供了许多那些我们误以为只能顶着断断续续的 Github 网络才能下载的软件。 下面这张列表主要来自山东大学的镜像站中的「常用软件」和南京大学的「github-release」。我严重怀疑南京大学就是把整个 tuna 给搬了一遍过来。 山东大学 南京大学 清华大学 balena-io/etcher https://mirrors.nju.edu.cn/github-release/balena-io/etcher/LatestRelease/ https://mirrors.tuna.tsinghua.edu.cn/github-release/balena-io/etcher/LatestRelease/ drawio-desktop https://mirrors.sdu.edu.cn/github-release/jgraph_drawio-desktop/ git-for-windows https://mirrors.sdu.edu.cn/github-release/git-for-windows_git/ https://mirrors.nju.edu.cn/github-release/git-for-windows/git/LatestRelease/ https://mirrors.tuna.tsinghua.edu.cn/github-release/git-for-windows/git/LatestRelease/ Krita https://mirrors.nju.edu.cn/kde/stable/krita/ libreoffice https://mirrors.tuna.tsinghua.edu.cn/libreoffice/libreoffice/ Magisk https://mirrors.nju.edu.cn/github-release/topjohnwu/Magisk/LatestRelease/ https://mirrors.tuna.tsinghua.edu.cn/github-release/topjohnwu/Magisk/LatestRelease/ Motrix https://mirrors.sdu.edu.cn/github-release/agalwood_Motrix/ obs-studio https://mirrors.sdu.edu.cn/github-release/obsproject_obs-studio/https://mirrors.sdu.edu.cn/software/Windows/OBS%20Studio/ https://mirrors.nju.edu.cn/github-release/obsproject/obs-studio/LatestRelease/ https://mirrors.tuna.tsinghua.edu.cn/github-release/obsproject/obs-studio/LatestRelease/ office tool plus https://mirrors.sdu.edu.cn/github-release/YerongAI_Office-Tool/ picgo https://mirrors.sdu.edu.cn/github-release/pbatard_rufus/ rufus https://mirrors.sdu.edu.cn/software/Windows/Rufus/ https://mirrors.nju.edu.cn/github-release/pbatard/rufus/LatestRelease/ https://mirrors.sdu.edu.cn/software/Windows/WePE/ventoy https://mirrors.sdu.edu.cn/github-release/ventoy_Ventoy/ https://mirrors.nju.edu.cn/github-release/ventoy/Ventoy/LatestRelease/ virtualbox https://mirrors.nju.edu.cn/virtualbox/ https://mirrors.tuna.tsinghua.edu.cn/virtualbox/ vlc https://mirrors.nju.edu.cn/videolan-ftp/ https://mirrors.tuna.tsinghua.edu.cn/videolan-ftp/ winehq https://mirrors.nju.edu.cn/winehq/ https://mirrors.bfsu.edu.cn/winehq/ wepe https://mirrors.sdu.edu.cn/software/Windows/WePE/

2022/1/19
articleCard.readMore

在Fedora搭建jekyll环境——dnf module

起因 我之前的博客一直用的是这个主题,直接使用 Fedora 官方源里的 rubygem-jekyll 似乎无法正常安装 Gemfile 中的依赖。之前使用 Archlinux 的时候,我是直接从 AUR 安装了一个 ruby-2.6 来使用的,但最近转到 Fedora 以后似乎就没法用这样的方案来解决了。 好在天无绝人之路,Fedora 也提供了安装老版本的 ruby 的方案——使用 dnf 的 module 功能。 关于 dnf module 关于 dnf 的 module 功能到底是用来做什么的,其实我并不清楚。虽说 Fedora 提供了文档,但就凭我的读中文文档都吃力的水准,似乎没有办法通过英文文档来理解这个全新的概念,所以我选择直接莽过去。 就我目前的理解而言,dnf 的 module 似乎并不致力于帮助用户完成系统内某一程序的新老版本共存的难题,而仅仅是给用户提供了停留在老版本软件的权利。module 所负责的,是保证老版本的程序能在你的系统上正常运行起来,而不会因为其他组件的更新而导致老版本的程序无法正常使用。 基本的使用方法 通过下列命令可以查看目前所支持的 module sudo dnf module list 通过下列命令可以选择 module 所要停留的版本( 以 ruby 2.7 为例 ) sudo dnf module enable ruby:2.7 通过下列命令可以取消锁定 module 程序所要停留的版本( 以 ruby 为例 ) sudo dnf module reset ruby 开始配置该 jekyll 主题的运行环境 sudo dnf module install ruby:2.7 sudo dnf install ruby-devel cd /path/to/the/jekyll-blog/ bundle install --path vendor/bundle 完成后,我们即可在 jekyll-blog 目录下 使用 bundle exec jekyll 来正常运行 jekyll 了。试着跑一下 bundle exec jekyll server 参考材料 Fedora Docs openSUSE 中文社区主页贡献指南 Switching to use Ruby 2.7 (or older) in Fedora 34 using DNF Modules 「WebArchive」

2022/1/12
articleCard.readMore

pacman更新时遇到「GPGME 错误:无数据」

情景再现 当初是使用 pacman 更新时遇上了「GPGME 错误:无数据」的问题,我尝试复现了下,大概是下面这样的情况。 [zhullyb@Archlinux ~]$ sudo pacman -Syu 错误:GPGME 错误:无数据 错误:GPGME 错误:无数据 错误:GPGME 错误:无数据 :: 正在同步软件包数据库... core 137.6 KiB 598 KiB/s 00:00 [------------------------------------] 100% extra 1566.0 KiB 6.12 MiB/s 00:00 [------------------------------------] 100% community 6.0 MiB 20.6 MiB/s 00:00 [------------------------------------] 100% 错误:GPGME 错误:无数据 错误:GPGME 错误:无数据 错误:GPGME 错误:无数据 错误:未能同步所有数据库(无效或已损坏的数据库 (PGP 签名)) 英文版的提示应该是长成下面这个样子 [zhullyb@Archlinux ~]$ sudo pacman -Syu error: GPGME error: No data error: GPGME error: No data error: GPGME error: No data :: Synchronizing package databases... core 137.6 KiB 574 KiB/s 00:00 [------------------------------------] 100% extra 1566.0 KiB 5.66 MiB/s 00:00 [------------------------------------] 100% community 6.0 MiB 18.1 MiB/s 00:00 [------------------------------------] 100% error: GPGME error: No data error: GPGME error: No data error: GPGME error: No data error: failed to synchronize all databases (invalid or corrupted database (PGP signature)) 解决方案 sudo rm /var/lib/pacman/sync/*.sig 很简单,就这一条命令就够了。 问题原因 pacman在更新数据库文件时也会尝试下载$repo.db.sig,这里的$repo可以是 core、extra、community、archlinuxcn 等仓库名。 但是无论是官方源还是 archlinuxcn 源,大多数源的数据库文件都不会被签名,也就不会存在 .db.sig 文件。 pacman 尝试下载时这些数据库文件的签名文件时,镜像站就会返回 404 的 http 状态码告诉pacman: “你个傻叉,神他妈没有这个文件!” pacman 挨了一顿骂,也就善罢甘休,没有再动这个念头,所以我们每次更新也都相安无事。 而出现这种错误的情况大多是发生在 校园网、酒店免费 WIFI 这种需要登陆以后才能上网的网络环境。 因为 pacman 尝试下载 .db.sig 文件时被登陆网页劫持了(这点你们应该深有感受,如果你在这种网络环境下没有登陆,你无论访问什么网页都会被重定向到登录界面,http 的状态码此时是200,不是404)。从没见过 .db.sig的 pacman 此时两眼放光,由于没有挨骂,他就迅速地把登录界面当成是.db.sig下载下来了。 下载下来以后,pacman 激动地摆弄起 .db.sig,甚至发现里面没有自己期待已久的 GPG签名数据并开始报错时仍然不愿意撒手,因此此时无论再怎么同步源码、再怎么 Syyu 也不会有效果,必须人工干预。

2022/1/1
articleCard.readMore

Cutefish的前世今生

CutefishOS是由我们国内的开发者主导(目前也主要是他们在开发)的桌面环境。不过似乎对于他的前世今生,似乎很多人都有误解。尤其是很多人认为他是一个Archlinux-based发行版;部分用户分不清他到底是基于Debian还是基于Ubuntu;还有人把它和 JingOS 弄混了。 先把这些问题的回答写在最前面: CutefishOS 是一个基于Debian的发行版,他的前身 CyberOS 是一个基于 Archlinux 的发行版。但要注意: Cutefish (不加OS)可以单独指代 CutefishOS 所使用的桌面环境,为了避免混淆,本文中我将使用CutefishDE来指代他的桌面环境。 CutefishOS 和 JingOS 目前只是官网互加友链的关系,并不是相同的东西。Cutefish的开发方向是基于qt重写一套UI,而JingOS则更像是在开发一套KDE的主题。 Cutefish的历史 CyberOS的故事 第一次体验到这个UI其实是在21年的3月,在Archlinux的QQ群里,群主向我们推荐了 CyberOS ,这是一个基于 Archlinux 的发行版。 由于基于 Archlinux,我直接就添加了CyberOS的源作为第三方源安装上了CyberDE,那会儿还顺手水了篇博客,由于后来事情发展太快,这篇博客早就不适用了,就干脆删了,现在在我的Github还能找到那会儿的存档。 更名CutefishOS 后来根据 CutefishOS 的QQ群的群主所说,是因为当时没注重海外平台的宣发,导致 CyberOS 的用户名在 Twitter 被抢注,因此决定改名 CutefishOS 。由于时间较为久远,QQ群的聊天记录已经几乎找不到了,我无法放出。 关于CutefishOS的创始时间我已经记不清了,但是可以推测是在21年的4~5月份左右。123 官网上线 21年5月12日,cutefishos.com 上线,暂时不提供安装镜像。 进军Arch系 5月26日,CutefishDE进入 Archlinux官方源。 同日,Github 组织 manjaro-cutefish 放出了使用 CutefishDE 的 manjaro安装镜像。这个组织和官方的 github.com/cutefishos 没有共同维护者,因此可以基本断定是第三方打包的。 Ubuntu第三方打包版的跟进 大约在7月中旬左右4,Github出现了一位名为 cutefish-ubuntu 的用户,开始在 Ubutnu 上编译 CutefishDE ,并通过 GithubPages 发布安装镜像,依然是第三方打包的安装镜像。 官方版本释出 21年国庆长假期间,cutefishos.com 释出由 Cutefish官方发布的基于Debian的CutefishOS镜像,搭载的DE是 0.5 版本的,英文版网页提供 Google Drive 和 Mega 的下载链接,中文版本网页非常贴心地添加了使用飞书下载的方式方便国内用户下载。 RPM系的跟进 COPR copr上分别有三名用户打包了CutefishDE/CyberDE,我以表格形式简单罗列一下 用户名 打包的DE 第一次打包日期 rmnscnce cutefish 2021.8.19 cappyishihara cyber 2021.11.17 jesonlay cutefish 2021.12.06 论坛用户 21年12月2日,一为名为gesangtome的网友在CutefishOS的论坛上发布了自己编译的CutefishDE。 Footnotes 通过whois查询得知cutefishos.com 这个域名注册时间为21年3月31日 ↩ Cutefish进入Archlinux官方源是在5月26日 ↩ CyberOS的Github仓库最后一次内容变更是在21年的5月23日 ↩ 这里参考的是 cutefish-ubuntu/cutefish-ubuntu.github.io 仓库的第一个commit的时间 ↩

2021/12/12
articleCard.readMore

wolai再打包遇到的问题--electron应用的dev判断机制

之前对于electron懵懵懂懂的时候就把 wolai 给打包上了 AUR ,那会儿年少无知,也不懂得把内置的 electron 拆开来换成系统内置的以节省空间。前一阵子给CN源打完 Motrix 以后突然想起来自己在 AUR 上还有维护一个叫 wolai 的electron 应用,于是打算把软件内置的 electron 拆出来。尝试使用 electron /path/to/app.asar 命令启动的时候发现了以下的问题。 虽然这个报错无关紧要,直接右上角叉掉也不影响软件正常使用,但是就这样推上 AUR 似乎有些不太妥当。于是使用搜索引擎查找答案。 发现是使用系统自带的 electron 启动时,app.asar 内置的一个叫 electron-updater 的模块在自动检测更新时会误认为我们此时处于开发模式,于是会尝试读取 app.asar 内部的 dev-app-update.yml 以查询更新。1 但问题在于这个 app.asar 并不是 wolai 开发者在开发时使用 development 模式打出来的包,应该是 production ,所以内置的那个文件名叫 app-update.yml ,少了个dev 前缀,就很尴尬。 以下内容来自一篇简书的文章2 所以调试的时候可以建一个default-app.yml文件放在D:\hzhh123\workspace\vue-work\electron-demo1\node_modules\electron\dist\resources\default_app.asar 下,这里就涉及到asar解压缩,但是这样会很麻烦,打包后也需要这样替换,麻烦,所幸electron-updater中提供了这个文件的属性配置updateConfigPath,可以通过设置这个属性来解决这个问题 很遗憾,我们并不是该应用的开发者,并不能指定electron-uploader构建时的参数,所以只能考虑解压缩 app.asar 手动放入 dev-app-update.yml 的方案。 根据又一篇简书的文章3,我们了解到 npm 中有一个叫 asar 的程序可以帮助我们解压缩 app.asar。我这里直接将内容搬过来 解压 asar extract 压缩文件 解压文件夹 压缩:如果压缩文件存在,则会被替换 asar pack 文件夹 压缩文件名 原文是让我们直接使用 npm 下载安装 asar 程序,然而这就会让打包过程变得很复杂,所幸 Archlinux 官方源中已经将这个程序打完了,我们可以直接将 asar 写入 makedepends。 大概就写成了这个样子。 asar extract ${srcdir}/squashfs-root/resources/app.asar ${srcdir}/new_app mv ${srcdir}/squashfs-root/resources/app-update.yml ${srcdir}/new_app/dev-app-update.yml asar pack ${srcdir}/new_app ${srcdir}/squashfs-root/resources/app.asar 程序正常启动,没有弹出之前的对话框了。 参考: Footnotes https://github.com/electron-userland/electron-builder/issues/1505 ↩ https://www.jianshu.com/p/15bde714e198 ↩ https://www.jianshu.com/p/17d97e6bf174 ↩

2021/12/3
articleCard.readMore

Typora与我

Typora 要收费了,$14.9 买断制,支持三设备激活。而且尚且不知道这里买断的是单个大版本更新还是多个大版本更新。 很多人说,不要紧,我们有VsCode、我们有Vnote、我们有MarkText。。。 但我还是不习惯。 Typora真的就是个非常纯粹的Markdown编辑器,他有所见即所得的视觉效果,同时为我提供了沉浸式的写作体验。 当我在使用Typora写文章的时候,我就是个非常单纯的内容创作者,我不需要去考虑各种Markdown的语法格式,我只需要用文字写下我所想的,然后通过右键菜单把文字的样式调整到一个能够合理突出主次的程度,便完成了。若是用的时间长了,记住了打开菜单时旁边现实的快捷键,那速度便更快了。即使有插入图片/视频的需求,我也只需要将图片复制进 Typora 的编辑框,我在Typora预先设置好的自定义上传命令会自动调用我部署在本地的PicUploader完成上传,并将媒体文件以 Markdown/html 语法呈现在编辑框中。 这样一来,我的行文思路就是连贯、不受打断的。即使需要从系统中截取一些图片作说明用途,我也可以通过 Flameshot 截取图片并简单画几个箭头、标几个序号或者框几个按钮后复制到剪切板,并最终粘贴到Typora的编辑框中,整个过程就像是我在和别人QQ聊天时截个图发过去一般简单。 倘若我使用别的Markdown编辑器,我便需要将图片保存到本地、手动上传到图床、手动写markdown的![]()语法,如此一来,我的精力就被分散了,那我也就不会有为文章插入图片的兴趣,抑或是插入完某张图片以后深感心力憔悴,便把写了一半的文章束之高阁,欺骗自己将来有一天我会继续完成这篇文章。 总而言之,Typora对于我而言确实是非常有用的工具,而我将在接下来的半年到一年时间中过渡到其他的开源Markdown编辑器中。即使改变我的使用习惯将是一件非常痛苦的事情,但我不得不这么做。Typora内置的electron在Archlinux的滚动更新下不知道过多久会出现与系统不兼容的情况1,所以这意味着继续使用老版本的Typora并不是长久之策,我需要在此之前尽快切换到其他的Markdown编辑器。而我不是个商业公司的Markdown工程师,单纯为了个人兴趣而花大价钱去买这一款生产力工具却无法得到经济回馈似乎并不是一个明智的选择。 反转了,仔细阅读Typora官网的Q&A后发现了这么一条: Can I use Typora for free ? You will have a 15 days free trial before the purchase. If you use dev version or Linux version, you will have unlimited trial time if you keep Typora updated. But we may show “trial button” or disable certain features to encourage you to purchase our app, but basic and most functions will be kept. 看起来 Dev 版和 Linux版本在最新版本可以无限试用下去,那我不考虑改变我的写作习惯了。 注: Dev 版藏得有点深,在这里 Footnotes baidunetdisk-bin内置的electron已经无法在如今的Archlinux上跑起来了,目前唯一的解决方案是拆包、并使用系统级的electron去启动百度网盘,也就是AUR的baidunetdisk-electron。但是typora运用了一些混淆/加密的手段,使得只有他内置的electron才可以正确启动程序。 ↩

2021/11/26
articleCard.readMore

我是来吹CloudflareMirrors的

Cloudflare也开始提供Linux开源镜像站了。 虽然在中国大陆地区,Cloudflare速度日常抽风,不适合作为我们本机镜像源,但完全可以用于境外VPS。平常我们对国内的镜像站比较熟悉,也知道自己的网络环境使用哪个镜像站会稍微快一些,但一旦出了国,这些经验就没有用了。 作为一家老牌的CDN网站加速服务提供商,Cloudflare提供的网络服务在全球范围内都非常快(嗯,对,全球范围不包含中国大陆) 无论你的vps是在美国日本,还是香港新加坡,cloudflare都能提供非常稳定高速的服务,只需要记住cloudflare镜像站的域名,便可以抛弃挑选镜像站的烦恼。 根据网页上所说,cloudflare会以「反代就近的镜像站」+「缓存」的形式来提供服务,既然都要通过cloudflare网络,那中国大陆地区就可以彻底别想了,能够给几乎所有地区提供不错的服务。目前说是只提供了「Archlinux」和「Debian」的服务,但是根据我考证下来,其实「Ubuntu」和「CentOS」也有,只不过没写在页面上罢了。那么废话不多说,我们上境外的vps测一下下载速度如何。 cloudflaremirrors 在我这台位于美国达拉斯机房的1Gbps机器上可以跑到80MB/s+的速度,虽然没有跑满理论速率,但也算是相当喜人的成绩了。 小结: CloudflareMirrors非常适合境外的vps使用,免去了用户自行给一个个镜像站测速的麻烦。

2021/11/21
articleCard.readMore

deepin-elf-verify究竟是何物?

起因 越来越多上架在 Deepin 应用商店中的 deb 包中开始依赖了一个叫做 deepin-elf-verify 的依赖,今天来讲讲这个神奇的 deepin-elf-verify 到底为何物,为什么这么多程序都要依赖于他来工作。 下载拆包 打开 Bfsu镜像站 ,可以很轻松地找到 Packages —— 在 apt 源中记录了各个文件信息(包括他在仓库中的相对位置)的这么一个神奇的文件,就是体积有点大,达到了68MB的样子。我们可以通过以下命令检索今天的主角——deepin-elf-verify。 curl -s https://mirrors.bfsu.edu.cn/deepin/dists/apricot/main/binary-amd64/Packages | grep deepin-elf-sign | grep pool 得到了输出: Filename: pool/main/d/deepin-elf-verify/deepin-elf-verify_0.2.0.6-1_amd64.deb 我们就可以把完整的下载链接拼出来: https://mirrors.bfsu.edu.cn/deepin/pool/main/d/deepin-elf-verify/deepin-elf-verify_0.2.0.6-1_amd64.deb 下载解压,大概是这么一个目录结构: deepin-elf-verify_0.2.0.6-1_amd64 ├── control.tar.xz ├── data.tar.xz └── debian-binary 是个常规的deb包该有的结构了。 control.tar.xz 中存放了deb包的相关信息 data.tar.xz 是整个包最终会被安装到系统中的文件 终于到了激动人心的时刻了,打开 data.tar.xz ! 搞错了,再来 打开UOS的源链接,使用curl+grep检索deepin-elf-verify在源中的相对位置 curl -sL https://uos.deepin.cn/uos/dists/eagle/main/binary-amd64/Packages | grep deepin-elf-verify | grep pool 获得输出: Filename: pool/main/d/deepin-elf-verify/deepin-elf-verify_0.0.14.5-1_amd64.deb Filename: pool/main/d/deepin-elf-verify/deepin-elf-verify-dbgsym_0.0.14.5-1_amd64.deb 拼接为链接: https://uos.deepin.cn/uos/pool/main/d/deepin-elf-verify/deepin-elf-verify_0.0.14.5-1_amd64.deb 下载后打开 data.tar.xz 说说结论吧 对于UOS 在UOS下,deepin-elf-verify用于检测用户运行的进程是否被deepin信任的证书签名过,虽然有些过于限制用户,对于一个将要广泛用于政府机关的发行版而言是可以理解的。 对于deepin deepin-elf-verify 在 deepin 上就是个空包。 当我们使用 deepin 安装一个含有 deepin-elf-verify 的软件包时,apt 会自动从源内搜索并安装 deepin-elf-verify,由于是个空包,他对于系统不会有任何负担。 大多数依赖deepin-elf-verify的程序都把依赖写成了deepin-elf-verify (>= 0.0.16.7-1),而在deepin源中,deepin-elf-verify版本号是 0.2.0.6,因此在未来的很长一段时间里应该都是满足要求的,说明统信那边并没有「想要让deepin装不上UOS的包」的这种想法,可见在这一点上,统信还没有明显的偏心。 在别的Deb发行版下 deepin-elf-verify存在于、并且仅仅存在于 deepin 和 UOS 的源内。 而当我们使用别的 deb 发行版(如Debian、Ubuntu)时,apt 无法在他们自己的源内找到 deepin-elf-verify ,apt就会报错并且停止安装。 小结: 至于其最终目的,是为了__________________________

2021/11/20
articleCard.readMore

【翻译】请别再使用主题装饰我们的软件

标题中的「我们」当然不是我自己,这是一封来自GNOME开发者针对广大GNOME社区开发者的一封公开信。看着挺有意思的,其中也透露出了GNOME的设计理念,我在这里尽力将其不掺杂个人情感地翻译完。原文可以查看这里: https://stopthemingmy.app/ 请从头到尾阅读这封信。 这份信针对的是那些在默认设置下使用第三方主题破坏软件体验的发行版,而不是那些试图使用第三方主题美化自己桌面的用户。(原文中出现的是tinkerers,意为修补匠) 我们是 GNOME 平台的应用开发者与设计者,我们为自己的成果感到自豪,并努力确保我们的应用能够为人们提供良好的体验。 然而不幸的是,在许多情况下,我们所有在软件的设计、开发、测试上所做出努力都因为第三方主题而变得徒劳无功。 GTK样式可以使得软件外观看上去不协调、甚至使得软件无法使用"> 图表包可以改变图标的含义,使得显示的图标无法准确的表达开发者的意思。"> 应用图标是一个软件身份的象征。改变一个软件的图标剥夺了开发者控制其品牌的可能性"> 注: 这些例子纯粹只是用于说明问题,并不针对个别主题。所以,主题开发者们别多想。❤️ 当然,还有些不那么直接的后果,包括: 在GNOME软件中心或Flathub 中使用的截图( Appstream Screenshots )中的UI会和你实际安装以后的UI看上去完全不同,这使得这些截图失去了原有的意义。 如果系统的UI元素和用户帮助文档中出现的元素不同,用户帮助文档将会极大地丢失原有意义。 这些博客文章更详细地解释了主题化的一些问题: GTK Stylesheets — Restyling apps at scale App Icons — Linux Themes & Third-Party Icons 这就是为什么我们心平气和地要求我们的软件不要被主题化。 它们是被上游所使用的(即默认的) GNOME 样式表、图标和字体 所构建和测试的,因此它们在用户的系统上应该是原汁原味的。 虽然我们可以直接在我们的应用程序中禁用主题,但我们不想这么做。 我们认为技术性的解决方案可能不会有效,因为这不是技术问题。 在技术上,我们希望软件可以在没有人工干预的情况下被自动地重新设计,但这到目前为止仍然是个幻想。在这种技术现状被改善之前,这种(应用被主题搞炸)的情况几乎不可能被解决。因此,我们正试图通过这封信向大家告知这种情况,并尽自己的一份力量。 如果你想要美化你自己的系统,我们没有意见。然而,如果你改变了诸如图标、样式表等东西,你应当意识到你的行为不会得到支持(应该是指不会得到社区的帮助)。您遇到的任何问题都应直接报告给主题开发者,而不是软件开发者。 作为一个平台,我们坚信GTK应当停止强制默认在所有软件使用同一个样式表(也就是说应该可以为不同的软件指定不同的GTK样式)。应用程序不必通过把样式表写死来避免这种情况,而是应该使用平台样式表(系统提供的样式表),除非他们魔改了样式表以加入其他内容。 我们意识到这是一个复杂的问题,但假设每个应用程序都适用于每个样式表同样也是一个糟糕的默认设置。 如果你是更改了系统样式表和图标的发行版的开发人员,希望你重新考虑此决定。 在没有任何 QA 的情况下更改第三方应用程序是鲁莽的,并且在任何其他平台上都是不可接受的。 您的行为对我们这些应用程序开发人员造成了很大的伤害,并且正在损害除了您的发行版以外的整个软件生态。 我们理解发行版需要脱颖而出来吸引用户。但是,我们敦促您想办法在不剥夺我们代理权的情况下做到这一点。 我们厌倦了当人们告诉我们「这个主题魔改得还不错」时,我们必须为我们从未打算支持的设置做额外的工作。你绝对不会对 Blender、Atom、Telegram 或其他第三方应用程序做出这样的魔改。我们的应用程序使用 GTK 并不意味着我们可以接受别人对它们的魔改。 由于你要使用 GNOME 平台开发,我们预设「你希望这个软件生态是健康的」。如果现实确实如此,我们要求您停止使用主题装饰我们的软件的这一行为。 署名, Alexander Mikhaylenko Maintainer of Games Avi Wadhwa Maintainer of Organizer Bilal Elmoussaoui Maintainer of Authenticator, Icon Library, Contrast and Obfuscate Cédric Bellegarde Maintainer of Lollypop, Eolie, and Passbook Christopher Davis Core contributor to Fractal Daniel García Moreno Maintainer of Fractal and Timetrack Falk Alexander Seidl Maintainer of Password Safe Felix Häcker, Maintainer of Gradio/Shortwave, Fragments, and Remotely Forever XML Maintainer of Random Jan Lukas Gernert Author of FeedReader and NewsFlash Jordan Petridis Maintainer of Podcasts Julian Sparber Core contributor to Fractal, maintainer of Teleport Lains Maintainer of Notejot, Khronos, Dot Matrix, Quilter, and Emulsion Manuel Genovés Maintainer of UberWriter Maximiliano Sandoval Maintainer of Decoder and Lorem, core contributor to Password Safe Michael Gratton Maintainer of Geary Rafael Mardojai C.M. Maintainer of Blanket, Dialect, Share Preview and Webfont Kit Generator Sophie Herold Maintainer of Pika Backup Tobias Bernard Designer of Fragments and Podcasts (among others) Zander Brown Maintainer of Icon Preview The Bottles Developers The Pitivi Developers Note: Even though some of us are Foundation members or work on GNOME, these are our personal views as individuals, and not those of the GNOME Project, the GNOME Foundation, or our employers. 推荐阅读: 《libadwaita:修复 Linux 桌面的可用性问题》

2021/11/5
articleCard.readMore

Waydroid on KDE 初体验

在西木野羰基的博客中看到了其在Fedora中使用waydroid跑明日方舟的截图,心里有有些痒痒了,决定在Archlinux上尝试使用waydroid。 Waydroid是什么 Waydroid是一个基于lxc容器技术,用以启动完整安卓系统的方案。 默认使用了LineageOS-17.1,对应 Aosp10,相比起 anbox 显然是更加新了。 内核支持 waydroid需要内核提供Ashmem和binder支持,西木野羰基是使用的自己编译的内核。而我在使用Archlinux,因此直接使用linux-zen即可。 注: AUR上的linux-xanmod虽然也有这些模块支持,但是在编译时设置了psi=0以提升性能,而waydroid恰巧需要psi=1的支持,故不可使用。 安装 Archlinux已经有人将其打包上传到了AUR,我们直接安装即可。我使用的 AUR Helper 是 yay,所以直接 yay -S waydroid --noconfirm 再装个python-pyclip解决剪切板同步的问题 yay -S python-pyclip 下载Waydroid镜像 sudo waydroid init 这一步将会自动(从SourceForge)下载纯净的LineageOS镜像压缩包并解压,处于中国大陆网络环境的用户记得( ) 如果你需要Gapps,可以指定下载Gapps版本,但是这将需要你获取Android ID并向谷歌提交 Custom Rom 的 Gapps 申请。见这里 sudo waydroid init -s GAPPS 启用服务 这个没什么好说的,使用systemctl启动服务。 sudo systemctl start waydroid-container.service 开启waydroid waydroid session start 一些简单的使用技巧 如果你想直接展示整个系统界面,可以使用 waydroid show-full-ui 我们也可以用waydroid app launch ${package_name}的方式来启动单个应用(包名可以使用waydroid app list来获取 当然,可以直接在Linux环境里 安装 某个apk waydroid app install path/to/apkfile.apk F11有助于解决应用分辨率问题,左Alt有助于解决键盘无法输入的问题。 Github上有个脚本,可以帮助 安装OpenGapps/Magisk/arm转译库/获取Android ID。 牢骚时间 对AMD和英伟达的显卡支持都不太行 不能直接输入中文,还是得借助安卓系统内的输入法。 不自带arm转译库,通过脚本安装的转译库似乎兼容性挺差(至少我是成功打开什么arm软件 系统运行的流畅度还可以 相关的资料似乎有点少,官方的文档也没有写得太详细 Waydroid会自动在$HOME/.local/share/applications/为wayland内的安装应用添加Desktop文件(这让我有些反感 一些截图

2021/10/31
articleCard.readMore

PicUploader使用系列(二)——为KDE的dolphin添加右键快捷菜单

上一篇文章我们在Archlinux中成功部署了PicUploader的web端,本文我们来讲讲如何为KDE的dolphin添加右键快捷键上传,效果类似这样。(gif图来自PicUploader作者的博客) 创建.desktop文件 mkdir -p $HOME/.local/share/kservices5/ touch $HOME/.local/share/kservices5/picuploader.desktop 填上这段内容 [Desktop Entry] Actions=PicUploader; MimeType=image/jpeg;image/png; Type=Service X-KDE-Priority=TopLevel X-KDE-ServiceTypes=KonqPopupMenu/Plugin Icon=/var/www/image/favicon.ico [Desktop Action PicUploader] Name=Upload with PicUploader Name[zh_CN]=使用PicUploader上传 Icon=/var/www/image/favicon.ico Exec=php /var/www/image/index.php %F | scopy 注: 这里的 scopy 是我在下面自己创建的一段脚本,用以同时满足x11和wayland下的使用,如果你仅使用x11的话直接改成xclip -selection clipboard即可。 MimeType指的是文件类型。在这份desktop中,我仅指定了png和jpg文件在右键时会弹出picuploader的上传菜单,如果你需要更多文件类型的MimeType,你可以参考下gwenview的desktop都写了哪些文件类型。 MimeType=inode/directory;image/avif;image/gif;image/jpeg;image/png;image/bmp;image/x-eps;image/x-icns;image/x-ico;image/x-portable-bitmap;image/x-portable-graymap;image/x-portable-pixmap;image/x-xbitmap;image/x-xpixmap;image/tiff;image/x-psd;image/x-webp;image/webp;image/x-tga;application/x-krita;image/x-kde-raw;image/x-canon-cr2;image/x-canon-crw;image/x-kodak-dcr;image/x-adobe-dng;image/x-kodak-k25;image/x-kodak-kdc;image/x-minolta-mrw;image/x-nikon-nef;image/x-olympus-orf;image/x-pentax-pef;image/x-fuji-raf;image/x-panasonic-rw;image/x-sony-sr2;image/x-sony-srf;image/x-sigma-x3f;image/x-sony-arw;image/x-panasonic-rw2; 安装所需组件 通知提示 右下角弹出文字提示的功能依赖于libnotify sudo pacman -S libnotify --needed 复制到粘贴板 复制到粘贴板的功能依赖于xclip sudo pacman -S xclip --needed 考虑到我可能在 x11 和 wayland 之间反复横跳,仅仅一个xclip看起来满足不了我的需求 sudo pacman -S xclip wl-clipboard --needed 手糊了一段脚本,用以判断对应的运行环境并调用相应的粘贴板工具 /usr/bin/scopy --- #!/bin/bash if [ "$XDG_SESSION_TYPE" = "wayland" ]; then wl-copy elif [ "$XDG_SESSION_TYPE" = "x11" ]; then xclip -selection clipboard else echo "ERROR! You are using $XDG_SESSION_TYPE" fi 为/usr/bin/scopy授予运行权限 sudo chmod 755 /usr/bin/scopy 启用该动作菜单 kbuildsycoca5 处理普通用户无权写入logs的问题 sudo chmod 777 -R /var/www/image/logs/ 最终结果 参考链接 在 KDE Plasma 5 的 Dolphin 中添加一个右键动作菜单 PicUploader: 一个还不错的图床工具

2021/10/24
articleCard.readMore

PicUploader使用系列(一)——在Archlinux上使用Caddy部署PicUploader

之前找对大陆网络友好的图床时,找到了cloudinary,但是全英文界面对操作增加了不少难度,其页面也不是很简洁,让我一下打消了使用网页版的念头。通过搜索,找到了 PicUploader 这一方案,使用php编写,支持cloudinary的api。 作者在其博客中仅提供了nginx的部署方案,我参考其配置文件成功实现了在caddy下的部署,并且花费了数个小时排坑,故写下本文帮助后来者节省时间。 安装caddy和php-fpm以及所需的拓展 sudo pacman -S caddy php-fpm php-gd php-sqlite --needed 配置php-fpm 在/etc/php/php.ini启用PicUploader所需拓展 PicUploaer依赖于fileinfo、gd、curl、exif、pdo_sqlite拓展,可以使用php -m命令来查看目前加载成功了的插件。 ;extension=bcmath ;extension=bz2 ;extension=calendar - extension=curl + extension=curl ;extension=dba ;extension=enchant - extension=exif + extension=exif ;extension=ffi ;extension=ftp - extension=gd + extension=gd ;extension=gettext ;extension=gmp ;extension=iconv ;extension=imap ;extension=intl ;extension=ldap ;extension=mysqli ;extension=odbc ;zend_extension=opcache ;extension=pdo_dblib ;extension=pdo_mysql ;extension=pdo_odbc ;extension=pdo_pgsql - extension=pdo_sqlite + extension=pdo_sqlite ;extension=pgsql ;extension=pspell ;extension=shmop ;extension=snmp ;extension=soap ;extension=sockets ;extension=sodium ;extension=sqlite3 ;extension=sysvmsg ;extension=sysvsem ;extension=sysvshm ;extension=tidy ;extension=xmlrpc ;extension=xsl extension=zip 编辑/etc/php/php.ini以增加单文件上传大小限制 查出这个问题浪费了我整整4小时时间。 - upload_max_filesize = 2M + upload_max_filesize = 100M 编辑/etc/php/php-fpm.d/www.conf使其在运行时使用caddy用户。 --- ; Unix user/group of processes ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. - user = http + user = caddy - group = http + group = caddy ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on ; a specific port; ; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on --- --- ; Note: This value is mandatory. listen = /run/php-fpm/php-fpm.sock ; and group can be specified either by name or by their numeric IDs. ; Default Values: user and group are set as the running user ; mode is set to 0660 - listen.owner = http + listen.owner = caddy - listen.group = http + listen.group = caddy ;listen.mode = 0660 ; When POSIX Access Control Lists are supported you can set them using ; these options, value is a comma separated list of user/group names. ; When set, listen.owner and listen.group are ignored ;listen.acl_users = ;listen.acl_groups = --- 2022年1月14日更新:在 Fedora 尝试部署的时候遇到了新的坑,Fedora 的相应配置文件为 /etc/php-fpm.d/www.conf,相应修改如下 ; Unix user/group of processes ; Note: The user is mandatory. If the group is not set, the default user's group ; will be used. ; RPM: apache user chosen to provide access to the same directories as httpd -user = apache +user = caddy ; RPM: Keep a group allowed to write in log dir. -user = apache +group = caddy ; The address on which to accept FastCGI requests. ; Valid syntaxes are: ; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on ; a specific port; --- --- ; Set permissions for unix socket, if one is used. In Linux, read/write ; permissions must be set in order to allow connections from a web server. ; Default Values: user and group are set as the running user ; mode is set to 0660 -;listen.owner = nobody +listen.owner = caddy -;listen.owner = nobody +listen.group = caddy ;listen.mode = 0660 ; When POSIX Access Control Lists are supported you can set them using ; these options, value is a comma separated list of user/group names. ; When set, listen.owner and listen.group are ignored -listen.acl_users = apache,nginx +;listen.acl_users = apache,nginx ;listen.acl_groups = ; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect. ; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original ; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address ; must be separated by a comma. If this value is left blank, connections will be ; accepted from any ip address. ; Default Value: any listen.allowed_clients = 127.0.0.1 拉取 PicUploader 最新代码 首先创建一个用于存放代码的目录 sudo mkdir -p /var/www/ clone 最新源码 sudo git clone https://github.com/xiebruce/PicUploader.git /var/www/picuploader 将代码所有权转交给caddy用户 sudo chown -R caddy:caddy /var/www/picuploader 编辑Caddyfile caddy默认使用/etc/caddy/Caddyfile,因此如果你就部署这一个站点,直接修改这个就好了。 caddy的语法非常简洁易懂,因此我随手写了几行就能跑起来了。 下面是我用的Caddyfile,如果你在服务器上部署,请把http://api.picuploader.com更换为你服务器所需要绑定的域名(不带http协议头),caddy将自动为你申请ssl证书。 http://api.picuploader.com { root * /var/www/picuploader php_fastcgi * unix//run/php-fpm/php-fpm.sock { index dashboard.php } file_server { index index.php } handle_errors { root * /etc/caddy/error rewrite * /error.html templates file_server } } # Import additional caddy config files in /etc/caddy/conf.d/ import /etc/caddy/conf.d/* php我选择了监听本地unix//run/php-fpm/php-fpm.sock的方案,这个路径在上文的/etc/php/php-fpm.d/www.conf可以设置,如需查询,直接使用 grep listen\ = /etc/php/php-fpm.d/www.conf应该就能看见。 设置访问密码(可选) caddy2开始不允许在caddyfile中直接指定明文密码,因此我们需要用hash-password获取加密后的密码密文 caddy hash-password --plaintext <YourPassword> 再在Caddyfile中,加上 basicauth /* { <username> <hashed_password> } 修改hosts/设置DNS解析 由于 api.picuploader.com 这个域名不在我手里,而我只是想在本地使用,并不打算部署到服务器,因此修改hosts将这个域名解析到本地是个不错的选择。 sudo sh -c "echo '127.0.0.1 api.picuploader.com' /etc/hosts" 而你若是在服务器上部署,应当去设置DNS解析,这个应该不需要我多说。 开启服务 在Archlinux下,我习惯直接用systemd运行caddy和php-fpm以开机自启动。 sudo systemctl enable --now caddy php-fpm 最终测试 在浏览器内访问 api.picuploader.com ,如果能看到页面,就算是成功啦。 设置上传参数 见作者博客:PicUploader: 各图床获取上传图片参数的方法

2021/10/21
articleCard.readMore

Archlinux坚果云踩坑实录

在Archlinux上,坚果云似乎出现了一些问题。 安装 yay -S nutstore 这个没什么可说的,AUR还是Archlinuxcn都无所谓,都是一模一样的。 白屏 双击图标,咦?怎么白屏了? 看看AUR评论区,有人说nutstore-experimental修了? 对比了一下,就是改了改/opt/nutstore/conf/nutstore.properties sudo sed -i 's|webui.enable=true|webui.enable=false|' /opt/nutstore/conf/nutstore.properties 轻松解决 窗口太小不能登陆 桌面使用了暗色主题导致部分字体不清晰? 参考使用fakehome方案暂时解决跑在KDE暗色主题下的程序使用亮色字体的问题编写启动命令 bwrap --dev-bind / / --tmpfs $HOME/.config /usr/bin/nutstore 本地markdown文件的文件类型被识别成了「坚果云 Markdown」 这个是由于坚果云自作主张推广他自己并不好用的lightapp,写了几条 mime 的规则,如图 看来在我们的启动命令中也需要防止坚果云接触到$HOME/.local/share/这个路径,所以现在的启动命令得写成这样。 bwrap --dev-bind / / --tmpfs $HOME/.config --tmpfs $HOME/.local/share/ /usr/bin/nutstore 修改desktop文件,使其使用我们自己攥写的启动命令 首先,复制一份desktop文件到我们的 $HOME 目录下,好处是下次更新的时候我们所做的更改不会被包管理器覆盖。 cp /usr/share/applications/nutstore.desktop $HOME/.local/share/applications/ 再修改$HOME/.local/share/applications/nutstore.desktop [Desktop Entry] Encoding=UTF-8 Type=Application Terminal=false Icon=nutstore -Exec=/usr/bin/nutstore +Exec=bwrap --dev-bind / / --tmpfs $HOME/.config --tmpfs $HOME/.local/share/applications --tmpfs $HOME/.local/share/mime /usr/bin/nutstore StartupWMClass=Nutstore Name=Nutstore Name[zh_CN]=坚果云 Comment=Data Sync, Sharing, Backup Comment[zh_CN]=数据同步,共享和备份 Categories=Network;Application;

2021/10/2
articleCard.readMore

把老版的火狐顶栏UI带回来

在Firefox更新UI以后,我就一直感觉不太适应。顶栏的一个个标签页占用的体积达到了原来的1.5~2倍。Thanks to black7375/Firefox-UI-Fix ,我们得以把以前的顶栏找回来。 加载新的css clone该github项目并进入对应路径后,执行install.sh git clone https://github.com/black7375/Firefox-UI-Fix.git cd Firefox-UI-Fix.git ./install.sh 在接下来的对话中,我们选择Photon-Style,这是最接近老版UI的。 紧接着脚本会要求我们选择我们的Firefox数据文件夹,我们可以打开Firefox,在浏览器地址栏输入about:support查看到我们所使用的数据文件夹路径。 使用空格键选择我们的数据文件夹后,在终端上该路径开头处的[ ]中会被打上X,确认无误后,敲回车。 重启浏览器,顶栏就长成了这样。 添加主题 为了进一步模仿Firefox经典的配色,我们可以安装上这个主题,变成这样 禁用暗色模式 如果你的系统主题使用的是深色,导致了诸如TUNA镜像站自动为你启用了暗色模式,而你想禁用的话,之前通过修改about:config方案依然适用。

2021/10/1
articleCard.readMore

记录一次原创文章被抄袭

今天在网站自搜的时候偶然间发现了一个叫「程序员宝宝」的站点转载了我去年在知乎专栏上发的一篇文章《Ubuntu下对deepin-wine的使用详解》。 转载的质量并不高,超链接都没有转载上去,只有干巴巴的图片和文字。翻到结尾处,我一口老血喷出。 我就纳闷了,我作为原创博主,自己都没有给这篇文章挂上CC的版权协议,怎么就有人自称是原创给我挂上了CC协议,要知道我知乎还明确勾选了「转载需要申请」呢。 抱着吃瓜的心态在谷歌上搜索,我发现了五篇抄袭我的文章。CSDN三篇,还有「程序员宝宝」和「程序员宅基地」使用相同UI的、被我怀疑是机器人搬运的站点。 CSDN那边,我在页面页脚处找到了「在线客服」,注册帐号后联系了客服,客服反应非常迅速,5分钟内就对抄袭文章进行了下架处理,这点值得表扬。 至于「程序员宝宝」和「程序员宅基地」这两个站点,在他们的版权申明中写得很清楚。 如果你是文章作者: 请通过邮件联系我们,邮件内容包括: 待删除的文章链接 发件人是待删除文章作者的证明(如果发件人邮箱地址能证明你是文章作者,此项内容可以为空) 我们会在收到邮件后7个工作日内进行处理。 但我找遍了整个网站,根本没有发现站点方的邮箱。 不知诸位有何解决方案?

2021/9/21
articleCard.readMore

使用AUR(Helper)安装软件时究竟发生了什么?对于常见的构建错误如何解决?

虽然对于没有能力手动修改/编写PKGBUILD的Arch用户其实是不应该使用AUR中的包的,这些软件的PKGBUILD可以由个人随意发布,并不能保证安全性,但是作为Archlinux的特色,但随着AUR Helper的趋于便利,还是吸引了不少小白使用AUR。本文将主要讲一讲 AUR Helper 帮助我们安装软件时到底做了些什么事情,并提供一些使用AUR Helper构建时常见错误的解决方案。 PartⅠ基本原理 makepkg是如何工作的? 以钉钉举例,我们可以从AUR上使用 git clone https://aur.archlinux.org/dingtalk-bin.git 获取到由这个包的维护者为我们提前写好的构建脚本。他的目录大概是长成下面这个样子: dingtalk-bin ├── com.alibabainc.dingtalk.desktop ├── dingtalk.sh ├── .git ├── .gitignore ├── PKGBUILD ├── service-terms-zh └── .SRCINFO 其中,.git是git的工作目录,可以忽视。 .gitignore属于git的配置文件之一,我们也不用管。 PKGBUILD是这个目录下最重要的东西,是一个用于提供参数的脚本。makepkg通过执行PKGBUILD脚本来获取到参数,自动进行下载和构建过程。 service-terms-zh, com.alibabainc.dingtalk.desktop, dingtalk.sh是包里所需要用到的东西。 当我们cd到这个目录下执行makepkg时, makepkg会调用curl / git下载PKGBUILD中source=()部分中以http(s)协议头或者git+开头的链接,这些东西将会被下载到这个目录的src文件夹下。 对于curl下载的东西,makepkg会使用校验码核对下载到的文件是否完整、是否是当初这个包的维护者下载到的这一个。 校验通过后,makepkg会依次执行prepare(){}、build(){}、package(){}函数中的命令陆续完成准备、编译过程,并将最终要打进包里的文件放置到pkg文件夹下。 最后,makepkg将会将pkg文件夹的内容压缩成包。 AUR Helper 干了些什么 我们还是以钉钉为例,看看我们执行yay dingtalk-bin时到底发生了什么。 PartⅡ常见错误解决方案 如果有其他情况觉得可以完善的,欢迎在评论区留言。 1. base-devel 没有安装 正如上面所说的,没有安装base-devel组,赶紧补上! 由于base-devel并不是一个具体的包,而是由多个包构成的包组,其实并没有很好的方法来检测你是否已经安装。 所以如果你不确定,你也可以执行下面的命令来确保自己已经安装。 sudo pacman -S base-devel --noconfirm --needed 常见表现: ERROR: Cannot find the strip binary required for object file stripping. PKGBUILD: line XXX XXX: command not found 2. source源文件下载失败 网络问题 国内的网络问题不用多说了,大多数情况下都是Github连接不上。 最简单的解决方案就是把source里下载失败的东西通过特殊手段(比如你浏览器设置下代理,或者找找fastgit这种反代)下载下来以后直接扔到PKGBUILD所在的路径,然后手动执行makepkg -si。 -s代表自动下载makedepend,-i表示构建成功以后自动安装 yay存放PKGBUILD的默认路径是在$HOME/.cache/yay/$pkgname下面,具体可以参考我的另一篇关于yay的用法详解的博客。 我在这里再讲一种使用 fastgit 作为反代加速github下载的方法。如果觉得fastgit帮助到了你,你可以考虑给fastgit项目打钱。 当你的yay出现这个询问菜单时,(也就是出现Diffs to show/显示哪些差异?字样时) 我们再开一个终端,输入 sed -i "s|github.com|hub.fastgit.org|g" $HOME/.cache/yay/*/PKGBUILD 接着就下一步安装即可。 链接失效 这种情况多见于维护者维护不到位,上游放出了新版本包并删除老版本包以后维护者没有及时跟进的。你可以去逛一逛AUR的评论区查看解决方案,或者去查找上游的最新版本是多少,尝试更改PKGBUILD中的pkgver参数和checksum以后尝试makepkg。 需要手动下载 一般情况下是上游没有提供直链,makepkg无法自行下载,需要人工介入。解决方法同上面的网络问题 3. checksum 错误 上文已经提到过,checksum用于判断你所下载到的软件和维护者当初下载到的是否一致。但是有些情况下,确实是维护者忘了更新checksum值了,因此我们需要做一个判断。 打开.SCRINFO,找到checksum报错的那个文件的链接。 使用wget/curl等工具将他下载下来,可以通过md5sum+文件名的方式获取他们的md5值。连续下载两次,核对两次的检验值是否一致。 如果结果一致,那么说明并不是网络波动导致的检验值不符,而是维护者没有及时跟进导致的,你可以使用yay -S $pkgname --skipchecksums的方式跳过验证校验值的过程,或者你可以修改PKGBUILD中的校验值为"SKIP"来跳过某一文件的校验后手动makepkg。 4. tuna反代受限 tuna的服务器只有一个ip,但当使用他提供的AUR的反代服务时,全国的使用者都会被AUR认为是tuna这一个ip,过大的请求数量可能导致tuna的服务器超出AUR每天给每个ip限制的请求次数。 具体表现: Rate limit reached 解决方案,改回AUR的服务器,使用自己的ip进行请求 yay --aururl "https://aur.archlinux.org" --save 写在最后 关于AUR使用的更多细节可以阅读 《yay进阶》

2021/9/11
articleCard.readMore

使用fakehome方案暂时解决跑在KDE暗色主题下的程序使用亮色字体的问题

9月6日更新:AUR的wemeet-bin维护者sukanka已经将咱的运行指令直接打进了包内,故本文已经基本失去原本的应用意义,但仍可以作为一个案例来解决类似问题。 在使用腾讯最近推出的Linux原生腾讯会议的时候,咱遇到了个十分影响体验的问题。 我在使用KDE的暗色主题,腾讯回忆自作主张将字体颜色调成了白色。然而,字体背景是白色的没,因此导致对比度下降,字体难以辨认。效果大概是这个鬼样子: 然而我一时半会儿却找不到合适的变量在运行腾讯会议之前unset,无法指定它使用一个正确的字体颜色。 此时,我想到了fakehome的解决方案——bwrap。 关于bwrap,依云在ta的博客里讲过运行原理,我在这里直接摘一小段过来 bwrap 的原理是,把 / 放到一个 tmpfs 上,然后需要允许访问的目录通过 bind mount 弄进来。所以没弄进来的部分就是不存在,写数据的话就存在内存里,用完就扔掉了。 而我们要做的,就是开一个tmpfs作为$HOME/.config,让腾讯会议读取不到KDE的主题配置文件。 使用如下命令 bwrap --dev-bind / / --tmpfs $HOME/.config wemeet 软件启动确认没有问题后,我们可以更改腾讯会议desktop中的启动命令 sudo $EDITOR /usr/share/applications/wemeetapp.desktop 将Exec=后面的命令改成我们刚刚启动所使用的命令即可。 关键词: bwrap, linux, 暗色模式, 深色模式, 夜间模式, 白色字体, 亮色字体

2021/9/5
articleCard.readMore

来,从AUR给Fedora偷个包

前一阵子,某Q群里的某初中生居然跳上了Fedora这辆灵车,还一直缠着我要我给他整个打rpm包的教程,说什么要复兴FedoraCN之类的我听不懂的话。碰巧听说Fedora似乎还没有wechat-uos,于是我就寻思着给Fedora打一个,顺便熟悉一下dnf的操作。 事实上,Fedora和Archlinux的目录结构很相似,理论上来讲Archlinux的大部分包都可以直接解压后塞到Fedora里直接用,对于咱这种日常偷Deb包的Arch用户来说基本没什么难度,唯一的难点在于处理依赖关系。 Tips1: 使用电脑端的访客可以在页面左下角打开侧栏以获取目录。 下载链接 如果你是为了wechat-uos这个包而非教程而来的,下载链接在这里。https://zhullyb.lanzoui.com/ikN55rqr7ah 偷包环境 Archlinux实体机(打包) Fedora虚拟机(依赖查询、测试) 准备好wechat-uos 首先,咱们先在Archlinux上把我们的wechat-uos先打包好,这个老生常谈的问题我不多赘述了。 yay -S wechat-uos --noconfirm 查找wechat-uos在Archlinux上所需的依赖 再去查看wechat-uos所需要的依赖 [zhullyb@Archlinux ~]$ yay -Si wechat-uos :: Querying AUR... Repository : aur Name : wechat-uos Keywords : electron patched uos wechat weixin Version : 2:2.0.0-1145141919 Description : UOS专业版微信 (迫真魔改版) URL : https://www.chinauos.com/resource/download-professional AUR URL : https://aur.archlinux.org/packages/wechat-uos Groups : None Licenses : MIT Provides : None Depends On : gtk2 gtk3 libxss gconf nss lsb-release bubblewrap Make Deps : imagemagick Check Deps : None Optional Deps : None Conflicts With : None Maintainer : DuckSoft Votes : 16 Popularity : 0.603501 First Submitted : Wed 30 Dec 2020 12:21:51 PM CST Last Modified : Sat 20 Feb 2021 06:53:24 AM CST Out-of-date : No 查找Fedora上的对应依赖包名 然后我们需要去Fedora上找一找这些依赖在Fedora上的包名都叫什么。 比如这个bubblewrap,我们需要的是他提供的bwrap,所以我们直接在Fedora上sudo dnf provides bwrap 再比如gconf并没有在/usr/bin路径下直接留下什么非常具有代表性的可执行文件,所以在Fedora里面寻找等效包就稍微复杂一些,但也并非不能找。 先在Archlinux下使用pacman -Ql gconf,输出结果有点长,我就截一小段上来。 [zhullyb@Archlinux ~]$ pacman -Qlq gconf /etc/ /etc/gconf/ /etc/gconf/2/ /etc/gconf/2/evoldap.conf /etc/gconf/2/path /etc/gconf/gconf.xml.defaults/ /etc/gconf/gconf.xml.mandatory/ /etc/gconf/gconf.xml.system/ /etc/xdg/ /etc/xdg/autostart/ /etc/xdg/autostart/gsettings-data-convert.desktop /usr/ /usr/bin/ /usr/bin/gconf-merge-schema /usr/bin/gconf-merge-tree /usr/bin/gconfpkg /usr/bin/gconftool-2 ...... 可以发现,gconf还是有不少文件是非常具有代表性的,比如这里的/usr/bin/gconf-merge-tree,我们在Fedora上使用sudo dnf provides gconf-merge-tree很容易就能找到对应的包是GConf2。 lsb-release这个依赖中,我们只是需要/etc/lsb-release这个文件存在让我们的bwrap可以顺利地伪装成uos的样子。Fedora中虽然有redhat-lsb-core这个包算是lsb-release的等效包,但是并不提供这个文件,因此我们只需要在待会儿打包的时候带一个/etc/lsb-release的空文件即可,不需要将redhat-lsb-core写进依赖。 最终我们可以确定下来需要的依赖为gtk2,gtk3,libXScrnSaver,nss,bubblewrap,GConf2。 准备打包 安装rpm-tools sudo pacman -S rpm-tools 生成工作路径 mkdir -pv $HOME/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} 编写 spec 文件 Name: wechat-uos Version: 2.0.0 Release: 1 Summary: A wechat client based on electron. License: None URL: https://www.chinauos.com/resource/download-professional Packager: zhullyb Requires: gtk2,gtk3,libXScrnSaver,nss,bubblewrap,GConf2 AutoReqProv: no %description %prep %pre %post %preun %postun %files /etc/lsb-release /opt/wechat-uos/ /usr/bin/wechat-uos /usr/lib/license/libuosdevicea.so /usr/share/applications/wechat-uos.desktop /usr/share/icons/hicolor/128x128/apps/wechat.png /usr/share/icons/hicolor/16x16/apps/wechat.png /usr/share/icons/hicolor/256x256/apps/wechat.png /usr/share/icons/hicolor/48x48/apps/wechat.png /usr/share/icons/hicolor/64x64/apps/wechat.png 处理source 一般来说,我们需要配置各种奇奇怪怪的编译命令,但是我这里直接选择了打包二进制文件,一来是减少了对于spec的学习成本,二来是因为wechat-uos本来就不开源,也没什么好编译的。 创建我们wechat-uos的二进制文件所需要放入的文件夹。 mkdir $HOME/rpmbuild/BUILDROOT/wechat-uos-2.0.0-1.x86_64 将我们的wechat-uos直接放入对应的文件夹中 补上我们的/etc/lsb-release mkdir $HOME/rpmbuild/BUILDROOT/wechat-uos-2.0.0-1.x86_64/etc/ touch $HOME/rpmbuild/BUILDROOT/wechat-uos-2.0.0-1.x86_64/etc/lsb-release 正式打包 rpmbuild -bb --target=x86_64 SPECS/wechat-uos.spec --nodeps 安装测试 sudo dnf install ./wechat-uos-2.0.0-1.x86_64.rpm 写在最后 rpm的打包工具是我近期最想吐槽的东西了,主要槽点有两个。 其一是:rpm在打包时的默认状态下,会使用 file 命令判断文件,如果是二进制的,用ldd判断依赖;如果是脚本,过滤文件中对应的 use/requires/import 语句,以此来找出内部依赖。**这固然是个非常贴心的小善举,**能够确保软件正常运行,但完全有可能造成比较奇怪的问题。比如我本次打包中rpm自作聪明地给我添加了一个libffmpeg.so的依赖,这东西整个Fedora自带的四个源里都不存在,在安装测试的时候出现了找不到依赖的情况。想我这种添加了找不到依赖的情况还算是运气好的,之前听说有人在使用opensuse的某个私人源的时候发现安装网易云音乐居然吧wps-office都给依赖上了,我想就是rpm自动检测到了网易云需要某个库,而wps恰好自带了这个库而导致的依赖误报。在Archlinux中,我们有一个叫namcap的小工具能够使用类似的方法检测软件运行时可能所需要的内部依赖,但他并不会默认启用,更不会自说自话的就直接把他添加为依赖,连一声也不吭。 其二是:rpm检测新增包内文件是否与系统已安装的软件包内的文件因为使用相同路径而冲突时,不仅会核对是否有冲突的同路径同名文件,他还会核对文件夹的文件占用情况。这说起来可能会有些抽象,我举个例子。在Fedora中,/usr/bin路径是被filesystem这个包所占有的,所以其他包在打包时是不能直接使用/usr/bin、/usr、甚至/来限定包内文件的范围的(也就是上面spec文件中的%files区域)。而我在第一次打包时,想要直接打包BUILDROOT下的所有文件,于是%files就直接填写了/作为限定,安装时提示/usr/bin和/usr/lib被filesystem这个包所占用,文件冲突。为此我还特意去仔细对比了Fedora自带的filesystem和我这个wechat-uos是否有冲突的文件,实则证明并没有,只是单纯这个检测机制过于死板罢了。而在Archlinux中,pacman安装时只会检测包内的文件是否与系统内的现有文件路径产生冲突,而不会非常无意义的去限定哪个文件夹是属于哪个包的。 附上本文的参考资料 为了避免源网页失效,我特意去互联网档案馆做了备份 「RPM打包原理、示例、详解及备查」 「Archive」 「在 Ubuntu 下直接将二进制文件制作成 rpm 包」 「Archive」 「解除RPM包的依赖的方法」 「Archive」 本文同时发布于「知乎专栏」,如果你恰好有知乎帐号的话或许可以考虑帮我点个赞?

2021/7/23
articleCard.readMore

下载一份openharmony的源码

不知道为什么,总是有人告诉我鸿蒙已经开源了,不信可以自己去看源码balabala,其实鸿蒙的手机端目前为止依然没有开源,或者说没有完整完整开源。本文我将介绍如何拉取一份openharmony开源的源码。 首先需要准备以下东西 一台装有类unix环境的电脑(wsl大概也行) 6G磁盘剩余空间 互联网(如果使用手机流量的话大概是1.5G) 安装git 没什么好说的,不再赘述。 设置git用户名和邮箱 git config --global user.email "you@example.com" git config --global user.name "Your Name" 下载repo(这个大多数发行版自己都有打包,但是都比较滞后,不如直接下载最新版的二进制文件设置好path变量直接用) mkdir -p ~/bin curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo chmod a+x ~/bin/repo cat >> ~/.bashrc <<EOF # set PATH so it includes user's private bin if it exists if [ -d "\$HOME/bin" ] ; then PATH="\$HOME/bin:\$PATH" fi EOF source ~/.bashrc 新建一个文件夹以同步源码 mkdir openharmony 进入这个文件夹 cd openharmony 初始化repo repo init -u https://gitee.com/openharmony/manifest.git --depth=1 ​ 注: --depth=1是为了仅保留一层commit记录,防止过多的历史commit占用空间,如果你想保留历 史commit,那可以把这里的--depth=1去掉。 使用repo正式开始同步源码 repo sync repo在sync的时候其实可以加很多选项,可以通过repo help自行研究,我自己常用的是repo sync --force-sync --current-branch --no-tags --no-clone-bundle --optimized-fetch --prune -j$(nproc --all) -f1 看到以下提示代表同步成功 repo sync has finished successfully. 后话 结果就当源码下载好并开始checkout后,出现了以下错误 Garbage collecting: 100% (220/220), done in 1.204s Updating files: 100% (35/35), done. Updating files: 100% (27/27), done. git-lfs filter-process --skip: line 1: git-lfs: command not found fatal: the remote end hung up unexpectedly error.GitError: Cannot checkout device_hisilicon_modules: Cannot initialize work tree for device_hisilicon_modules error: Cannot checkout device_hisilicon_modules 看着error很容易可以发现是我的系统没有git-lfs的原因,看样子openharmony使用了git-lfs来储存了某个大文件。 sudo pacman -S git-lfs #别的发行版请自行查找相关安装方法 于是乎,安装好git-lfs重新sync源码 oepnharmony目录下,.repo文件夹内是你从git服务器上下载下来的原始数据,repo将在所有数据下载完成以后将他们自动checkout成代码。 源码结构是下面这个样子 . ├── applications ├── base ├── build ├── build.py -> build/lite/build.py ├── build.sh -> build/build_scripts/build.sh ├── developtools ├── device ├── docs ├── domains ├── drivers ├── foundation ├── .gn -> build/core/gn/dotfile.gn ├── interface ├── kernel ├── prebuilts ├── productdefine ├── .repo ├── test ├── third_party ├── utils └── vendor 18 directories, 3 files 我提供个参考数据,AOSP源码不含.repo原始数据的大小是40G,就openharmony这个代码量,恐怕很难让我相信这是一个兼容安卓应用的系统的完整代码。

2021/6/6
articleCard.readMore

在Windows与Linux双系统下共享蓝牙鼠标

我自己使用的鼠标是一只小米的无线蓝牙双模鼠标。但是由于我的USB接口不是很充裕,我平时还是蓝牙鼠标用的比较多。 但是,每当我在Windows和Archlinux上切换时,我不得不重新配对我的蓝牙鼠标。原因我在翻译Archwiki上关于蓝牙鼠标相关叙述时已经解释得非常清楚了,我摘在下面: “首先,计算机保存蓝牙设备的 MAC 地址和配对密钥;然后,蓝牙设备保存计算机的 MAC 地址和配对密钥。这两步通常不会有问题,不过设备蓝牙端口的 MAC 地址在 Linux 和 Windows 上都是相同的 (这在硬件层面上就设定好了)。然而,当在 Windows 或 Linux 中重新配对设备时,它会生成一个新密钥,覆盖了蓝牙设备之前保存的密钥,即与 Windows 配对产生的密钥会覆盖原先与 Linux 配对的密钥,反之亦然。“ 先在Linux上连接蓝牙鼠标,再重启到Windows重新配对蓝牙蓝牙鼠标。 到微软官网下载PsExec.zip,解压后,记住你所解压的路径。 在Windows中,使用管理员权限打开cmd.exe cd到PsExec解压目录,使用如下命令将我们所需要的蓝牙密钥信息保存到C盘根目录下。 psexec.exe -s -i regedit /e C:\BTKeys.reg HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Services\BTHPORT\Parameters\Keys 根目录的BTkeys.reg可以直接用记事本打开,内容大概是下面这个样子 为了方便后面的解说,我用各种颜色标注了起来。 在Linux下获取su权限以后,我们需要将Linux下随机分配给鼠标的蓝牙地址改成在Windows上获取的那个地址。上图中「红部分」划出来的就是Windows下获取的地址。 [zhullyb@Archlinux ~]$ su Password: [root@Archlinux zhullyb]# cd /var/lib/bluetooth/E0\:94\:67\:74\:0D\:5F/ [root@Archlinux E0:94:67:74:0D:5F]# ls C6:2A:1B:33:2E:71 cache settings [root@Archlinux E0:94:67:74:0D:5F]# mv C6\:2A\:1B\:33\:2E\:71/ C4\:F6\:B3\:2C\:BD\:7E 再编辑/var/lib/bluetooth/<本机蓝牙地址>/<鼠标蓝牙地址>/info 原文件如下: [General] Name=MiMouse Appearance=0x03c2 AddressType=static SupportedTechnologies=LE; Trusted=true Blocked=false WakeAllowed=true Services=00001530-1212-efde-1523-785feabcd123;00001800-0000-1000-8000-00805f9b34fb;00001801-0000-1000-8000-00805f9b34fb;0000180a-0000-1000-8000-00805f9b34fb;0000180f-0000-1000-8000-00805f9b34fb;00001812-0000-1000-8000-00805f9b34fb; [IdentityResolvingKey] Key=067764BF59A7531E978AFDC6BB5EC8E1 [LongTermKey] Key=E3C49B4F3256018192942EB0CDDEE6A3 Authenticated=0 EncSize=16 EDiv=28209 Rand=15970850852728832717 [DeviceID] Source=2 Vendor=10007 Product=64 Version=40 [ConnectionParameters] MinInterval=6 MaxInterval=9 Latency=100 Timeout=600 「黄色部分」LTK 对应 LongTermKey 下的 Key,把小写转换成大写并删去逗号即可。 「绿色部分」ERand 对应 Rand。这里比较特殊的是,我们必须先将 Windows 中的值倒转过来再转换为 10 进制。即c2,83,7f,8f,7c,76,b4,02->02,b4,76,7c,8f,7f,83,c2->194910961239294914 「蓝色部分」EDIV 对应 EDiv。把 16 进制转换成 10 进制即可,这里就不用倒转了。 具体的转换方法我不再赘述,我把我的转换过程放在下面,我相信各位读者能够看懂。 [zhullyb@Archlinux ~]$ echo 'e3,c0,b2,8e,64,2b,12,16,d8,c2,d7,d4,59,55,92,cd' | tr a-z A-Z | sed 's/[[:punct:]]//g' E3C0B28E642B1216D8C2D7D4595592CD [zhullyb@Archlinux ~]$ echo $((16#02B4767C8F7F83C2)) #这里我是手动倒叙的 194910961239294914 [zhullyb@Archlinux ~]$ echo $((16#000055a3)) 21923 做完这些操作以后,sudo systemctl start bluetooth即可

2021/5/30
articleCard.readMore

选择最新的Archlinux镜像源

找到最新的Archlinux镜像源 我是testing+kde-unstable用户,平均每天更新4次,对于我而言,选择最新的Archlinux镜像是非常重要的。 Archlinux的主源并不开放给个人用户使用,仅开放给一级镜像站进行同步,因此我们需要手动寻找国内较新的镜像站。(理论上来说一级镜像站应该比二级镜像站更新,但是有些一级镜像站的同步频率并不高,同步延迟可能会比某些二级镜像站还要高) 一个archlinux的镜像目录大概是长下面这个样子 archlinux/ ├── community ├── community-staging ├── community-testing ├── core ├── extra ├── gnome-unstable ├── images ├── iso ├── kde-unstable ├── lastsync ├── lastupdate ├── multilib ├── multilib-staging ├── multilib-testing ├── pool ├── staging └── testing 其中的lastsync和lastupdate用unix时间戳记录着上一次同步时间和镜像的上一次变更时间。 因此,我们只需要对比各个镜像站的lastsync谁比较新就行了,我写了如下的辣鸡脚本 #!/bin/bash tuna=$(curl -s https://mirrors.tuna.tsinghua.edu.cn/archlinux/lastsync) bfsu=$(curl -s https://mirrors.bfsu.edu.cn/archlinux/lastsync) sjtug=$(curl -s https://mirror.sjtu.edu.cn/archlinux/lastsync) aliyun=$(curl -s https://mirrors.aliyun.com/archlinux/lastsync) ustc=$(curl -s https://mirrors.ustc.edu.cn/archlinux/lastsync) zju=$(curl -s https://mirrors.zju.edu.cn/archlinux/lastsync) cqu=$(curl -s https://mirrors.cqu.edu.cn/archlinux/lastsync) lzu=$(curl -s https://mirror.lzu.edu.cn/archlinux/lastsync) neusoft=$(curl -s https://mirrors.neusoft.edu.cn/archlinux/lastsync) dgut=$(curl -s https://mirrors.dgut.edu.cn/archlinux/lastsync) netease=$(curl -s https://mirrors.163.com/archlinux/lastsync) tencent=$(curl -s https://mirrors.tencent.com/archlinux/lastsync) hit=$(curl -s https://mirrors.hit.edu.cn/archlinux/lastsync) huaweicloud=$(curl -s https://mirrors.huaweicloud.com/archlinux/lastsync) sohu=$(curl -s https://mirrors.sohu.com/archlinux/lastsync) opentuna=$(curl -s https://opentuna.cn/archlinux/lastsync) pku=$(curl -s https://mirrors.pku.edu.cn/archlinux/lastsync) nju=$(curl -s https://mirrors.nju.edu.cn/archlinux/lastsync) njupt=$(curl -s https://mirrors.nju.edu.cn/archlinux/lastsync) echo """ $tuna #tuna $bfsu #bfsu $sjtug #sjtug $aliyun #aliyun $ustc #ustc $zju #zju $cqu #cqu $lzu #lzu $neusoft #neusoft $dgut #dgut $netease #netease $tencent #tencent $hit #hit $huaweicloud #huaweicloud $sohu #sohu $opentuna #opentuna $pku #pku $nju #nju $njupt #njupt """ | sort -r 其运行结果如下 1622248120 #neusoft 1622247879 #dgut 1622247698 #hit 1622246042 #zju 1622246042 #tuna 1622246042 #bfsu 1622242426 #sjtug 1622242426 #njupt 1622242426 #nju 1622240702 #ustc 1622240522 #cqu 1622238783 #netease 1622235120 #lzu 1622232241 #huaweicloud 1622230871 #tencent 1622217845 #aliyun 1622217001 #pku 1622203750 #sohu 1622166379 #opentuna 通过不同时刻的多次测试可以看出,国内同步频率最高的是东软(neusoft)的镜像。顺手一查,没错,是个一级镜像站。通过unix时间戳得知,东软的archlinux镜像几乎是每分钟同步一次,恐怖如斯。。。 获得更好的下载速度 我们已经得知东软是国内同步频率最高的Archlinux镜像站了,但是我用东软镜像站的下载速度并不太好看。此时,我们就要搬出依云大佬的神器——pacsync 在root用户下使用如下命令装载pacysnc后 echo '#!/bin/bash -e unshare -m bash <<'EOF' mount --make-rprivate / for f in /etc/pacman.d/*.sync; do filename="${f%.*}" mount --bind "$f" "$filename" done pacman -Sy EOF' > /usr/bin/pacsync 创建/etc/pacman.d/mirrorlist.sync指定我们用来同步pacman数据库(比如东软) /etc/pacman.d/mirrorlist中存放其他国内镜像源地址(按照同步速度从上到下) 以后的同步命令为 sudo pacsync && yay -Su 觉得命令过长的话设置alias可以是个不错的选择。

2021/5/29
articleCard.readMore

请给 tuna/ustc 镜像站减压

不知道从什么时候开始,我总觉得tuna的镜像站提供的下载速度越来越慢,直到我前几天翻开tuna镜像站的「服务器状态」,我被眼前的景象给震惊到了。 我在这里大致观察了一下这张图:服务器流量主要是由四个部分组成,「http-ipv4」、「https-ipv4」、「http-ipv6」和「https-ipv6」。光是从过去24小时的平均出站流量来计算的话,大约就是2.4Gb/s,如果观察图中的流量高峰期的话,大概是4Gb/s的一个速率。这个流量大小是什么概念呢?根据我个人浅薄的建站经验来讲,这个流量可以让大部分供应商把你的网站判断为正在遭受攻击,你将被强制进入黑洞模式。然而对于tuna的镜像站而言,这个流量速率确是日常。换句话说,tuna的服务器都相当于每时每刻都在被来自全国的开发者“攻击”。 来自 2022 年的竹林: 我是真没想到去年 tuna 的网络负载只有这点的。2022 年的负载图在下面附上,已经翻了一倍不止了 因此,我们也就不难理解为什么tuna近些年来经常出现断流等一系列问题了。 客观上 TUNA 和 UTSC 是国内知名度和镜像项目数量以及同步速度都靠前的镜像站,但也因为如此,这两个镜像站每日的带宽负载是很大的;能跑满我的本地带宽速度是较理想的情况,但是那么大的负载,时间跨度长了体验到的波动差异也就多了起来。至少在我这里,长时间使用两个镜像站的速度波动挺大的。TUNA 也曾微言过带宽日益不堪重负,所以从道德情感和技术理论等角度上,尽管他们是理想的镜像站点,我个人不会优先使用这两个镜像站,也不会优先推荐别人使用。 ——WPlanck 国内的开源镜像站我大多都已经收集到这一篇博客中了,以下几个镜像站是我重点推荐的。 bfsu tuna的姊妹站,通俗来讲就是tuna派人维护,北京外国语大学出钱。人少、稳定、涵盖项目较广。 sjtug 上海交大的站点,也有不少项目,据说sjtug上的manjaro镜像是国内几个开源镜像站中同步最勤快的,用的人也不多。 opentuna tuna那边用国内aws服务器搭的站点,速度超快,不过比较可惜的是现在同步的项目不多,同步频率低,大概是一天一次的样子。 pku 是不是没想到北大也有镜像站?没记错的话是今年三四月左右刚开的,和opentuna情况差不多,用的人少、速度快、同步的项目不多。 hit 哈尔滨工业大学的镜像站,速度我跑下来感觉一般,不是特别亮眼,不过同步频率高。

2021/5/27
articleCard.readMore

我为什么选择Archlinux?

对于我而言,我用Archlinux主要的原因就是实用主义。我可以很负责的说,Arch真的是在我所有用过的发行版当中最符合实用主义的一个了。 很多大佬一提到Archlinux就扯些什么kiss原则,在我看来则不然。 整洁规范的系统 规范代码为的不是为了什么Art of Code,而是可读性的提升;遵循kiss原则亦是如此。 配置文件的路径写好了,符合规范,我们就能一下子找到,我们是为了实用主义而遵守kiss原则。 同样的,我同样可以为了实用主义而破坏kiss原则。比如在我的archiso-zhullyb中,我添加了一个pacman的hook将我的定制内核重命名为linux以确保其能够正确被ventoy所识别,这也是为了实用主义。 那么,什么时候我会破坏kiss原则呢?当我认为破坏kiss原则所带来的利大于弊时,我就会考虑以一个并不规范但却有效的方法来处理问题。 但很有趣的是,由于Archlinux的官方总是将kiss奉为圣旨,这就给我们提供了一个非常nice的环境了——在一个非常规范的系统内,破坏kiss原则所带来的代价并不会很大,这就好比在一个布线整齐的机房内临时私拉两三根线并不会给维护带来多大的困难。 Archlinux对上游软件包的发行策略 不同于apt在源内提供了统一软件的多个版本供用户选择,pacman剑走偏锋,默认用户系统内所有软件都是最新的。 由此带来了一个好处——不会出现由于版本过高/过低导致的依赖问题。只要我保证系统内的所有软件都是最新的,就不会出问题,非常的简单粗暴。 此外,不考虑依赖版本这一特点对于打包人来说也是一种解脱。 pacman简单的打包方式 不同于deb以及rpm,pacman的软件包应该是所有发行版中最省事儿的。 打包软件时,我们只需要写(改)一份PKGBUILD,就可以仅仅通过在PKGBUILD所在的路径执行makepkg命令来完成一次打包,这相比起deb而言可谓是天差地别。如此简单有效的打包方式注定其将被实用主义者所青睐。 超低的社区贡献成本 很多发行版社区开发与贡献其实并不容易参与进去,我拿Ubuntu来做个比较。 附: Archwiki是先斩后奏类型的文档,在你按下保存按钮的那一刻,wiki将立即被更新,所有访客都将看到你改动后的内容。wiki文档拥有变更记录,不担心有人恶意搞破坏,向wiki管理员提出举报后破坏者的账号会被及时封禁,wiki可以非常简单地回滚到之前的状态。 AUR同样也是,你可以随意上传自己的PKGBUILD,可以被别的用户及时看到。如果上传恶意脚本,在别的用户举报后你将迅速被封号。 Wiki方面 Ubuntu其实是我第一个上手的Linux发行版,在为期半年的Ubuntu体验中,我对于社区做出的贡献为0。这倒也不是我不热衷于参与社区贡献,而是对Ubuntu社区的贡献成本太高了。去贡献文档翻译,需要等待漫长的审核过程,在第一篇汉化文章正式展现在别的用户眼前后,我一定会被激发出继续翻译第二篇的热情。然而,面对太长的审核周期,再高涨的热情恐怕也会被浇灭。 Arch的社区则不一样,他并不像别的社区那样严谨——只要注册个wiki的账号便可以开始贡献文档。你可以随意地编辑一篇文章或者是新增一篇自己的文章,编辑后的文章将能够立即被别的用户所阅读到,没有任何审核过程,有了这份热情,我便继续翻译别的文档,我想,这应该就是archwiki为何涵盖面如此之广的原因。 AUR方面 同样也是拿Ubuntu对比。 在Archlinux下,我只需要简单的写一个PKGBUILD即可轻松构建一个软件包,同时,我也可以将这份有我攥写的PKGBUILD上传到AUR供别的用户使用。AUR作为一个公开的储存库,任何Arch用户都可以通过AUR Helper轻松得从AUR中获取我写的PKGBUILD并在本地打成自己的包。与此同时,我也可以创建一个私人源,直接发行我构建的二进制包。 Ubutnu则不然,他的打包方式则要麻烦得多,同时也没有类似PKGBUILD一样的东西便于用户分享自己的打包脚本。唯一能够分享自己的劳动成果的方式无非就是直接分享自己打出来的deb包,最多也不过是建立自己的ppa,这对于用户来说是极为麻烦的。用户需要处心积虑地寻找自己所需要的deb包或是含有目标包的ppa地址并手动添加,不像Archlinux有AUR这种东西能够让我们知道在哪里能够找到我们所需要的包。

2021/5/23
articleCard.readMore

使用vercel创建一个随机图片api

如果你的网络环境不算太差的话,你在访问我博客的时候应该可以看到顶部有一张背景图。假如你访问我的博客时留心观察,你或许会发现每次你访问我博客时的背景都是不一样的。如果你没玩够,或许你可以尝试点击这里,我总共搜集了20张壁纸供诸位赏玩。 是的,这是使用php实现的随机图片api,托管于vercel,你可以在aya的博客上找到我使用的代码。具体配置方式我不再赘述。 然而,我们还需要解决一个问题: php在哪里运行? 如果你拥有自己的服务器,在国内访问速度毫不逊色,那就好办了,直接扔自己服务器上即可。然而,我并没有。我需要找到一个在国内访问速度给力的地方来部署我的api,以确保访客在打开我的博客时可以在第一时间获取到图片的真实链接并开始加载。 起初,我将其部署在我的好朋友(你可以猜猜他是谁)的国内vps上,访问速度自然不用说。然而,他的服务器不支持https,这就导致使用chrome访问的时候chrome不会自动访问我的api,博客顶部一片惨蓝。。。 随后,我使用的是000webhost提供的虚拟主机,国内访问起来也还不错,大概正常运行了半个月左右的时间,然后莫名开始502了。我懂,作为不交钱的白嫖用户应该自觉滚蛋了,这点觉悟咱还是有。 随后,我找到了目前的方案——vercel vercel是被我用来部署静态网页的,但我没想到他也能支持php。参考了vercel-php项目后,我大致了解了整个仓库结构。 project ├── api │ └── index.php └── vercel.json php和附带的资源文件(如果有的话)一定要放到api文件夹下才能够正常被vercel识别。 以下附vercel.json { "functions": { "api/index.php": { "runtime": "vercel-php@0.4.0" } } }

2021/5/21
articleCard.readMore

禁止deepin-wine-tim使用simsun字体渲染

本文中,我通过bwrap命令对运行Tim的wine程序屏蔽了simsun字体以获得了一个更为舒适的字体渲染效果。我所使用的Tim为deepin-wine-tim,至于deepin-wine-qq通过相同的方式应该也能达到相同的效果,spark商店的Tim我自己测试下来似乎是没法达到这样的效果,而使用其他方法安装simsun字体的网友们则需要注意灵活变通,不要照抄我给出的字体路径。 在Archlinux下,我们通常会使用deepin-wine5来运行QQ/Tim. 但是当我们在系统中倒入simsun字体时,无论使用什么奇迹淫巧似乎都无法阻止deepin-wine5找到simsun并优先使用它。于是,字体渲染就会变成如图这样奇奇怪怪的画风: 但是我并不喜欢这样的渲染效果,使用simsun渲染出来的字体总感觉有一种上世纪的风格,况且,在我的1080p小屏下显示并不清晰。 于是,在尝试了更改注册表、在wine容器的系统路径下直接塞入字体文件等等方式无果后,我选择了逃避——直接让wine程序读取不到simsun。 我的simsun是通过ttf-ms-win10-zh_cn这个包安装上去的,被安装在/usr/share/fonts/TTF/路径下。 使用pacman -Qo /usr/share/fonts/TTF/命令查找这个路径下所安装的字体包,我这里的输出如下: [zhullyb@Archlinux ~]$ pacman -Qo /usr/share/fonts/TTF/ /usr/share/fonts/TTF/ is owned by ttf-cascadia-code 2102.25-1 /usr/share/fonts/TTF/ is owned by ttf-fira-code 5.2-1 /usr/share/fonts/TTF/ is owned by ttf-hack 3.003-3 /usr/share/fonts/TTF/ is owned by ttf-monaco 6.1-6 /usr/share/fonts/TTF/ is owned by ttf-ms-win10-zh_cn 2019ltsc-1 /usr/share/fonts/TTF/ is owned by ttf-opensans 1.101-2 可以看到,并没有什么对wine程序运行特别重要的字体包,于是我计划通过bwrap命令对运行Tim的wine程序直接屏蔽这个路径。 首先安装提供bwrap命令的bubblewrap程序: sudo pacman -S bubblewrap --needed 通过查找deepin-wine-tim的desktop文件发现Tim的启动命令是/opt/apps/com.qq.office.deepin/files/run.sh 在终端中输入命令进行测试bwrap --dev-bind / / --tmpfs /usr/share/fonts/TTF/ /opt/apps/com.qq.office.deepin/files/run.sh 出现如下界面,看来方法是可行的。 于是,我们进一步更改deepin-wine-tim的desktop文件,以方便我们不需要每次都在Terminal中执行这么一大长串命令。需要更改的地方如下图红色方框圈出部分 我这里附一下图中的命令方便诸位复制粘贴。 [zhullyb@Archlinux ~]$ cat /usr/share/applications/com.qq.office.deepin.desktop #!/usr/bin/env xdg-open [Desktop Entry] Encoding=UTF-8 Type=Application X-Created-By=Deepin WINE Team Categories=chat;Network; Icon=com.qq.office.deepin Exec=bwrap --dev-bind / / --tmpfs /usr/share/fonts/TTF/ /opt/apps/com.qq.office.deepin/files/run.sh Name=TIM Name[zh_CN]=TIM Comment=Tencent TIM Client on Deepin Wine StartupWMClass=tim.exe MimeType=

2021/4/27
articleCard.readMore

在系统使用暗色主题时禁用Firefox的夜间模式

在我使用Archlinux的时候经常会使用一些暗色主题,但是我并不希望我浏览网页时一些自作聪明的网页自动切换成夜间模式。 这个设置我找了好久,每次在谷歌上检索都会跳出来一堆教我改Firefox主题的、用插件开夜间模式的,却都不是我的目的。 我们所需要做的是在浏览器地址栏输入about:config进入高级设置 搜索并添加一个值 ui.systemUsesDarkTheme 将这个选项的数值设置为0即可。 2021.12.13更新: Firefox 更新 95.0 以后,如果遇到原方案失效的问题,可以参考 CSL的博客。

2021/4/23
articleCard.readMore

记一次在Gitlab部署Jekyll博客时遇到的jekyll-github-metadata报错问题

我的博客是挂在GitlabPages上的,在为博客更换主题的时候遇到了一点点小麻烦。 报错如图: 当然,我这边也会附上详细的报错日志,以便后人能够通过关键词搜索到。 Configuration file: /builds/zhullyb/test/_config.yml Source: /builds/zhullyb/test Destination: public Incremental build: disabled. Enable with --incremental Generating... Jekyll Feed: Generating feed for posts GitHub Metadata: No GitHub API authentication could be found. Some fields may be missing or have incorrect data. GitHub Metadata: Error processing value 'url': ERROR: YOUR SITE COULD NOT BE BUILT: ------------------------------------ No repo name found. Specify using PAGES_REPO_NWO environment variables, 'repository' in your configuration, or set up an 'origin' git remote pointing to your github.com repository. Cleaning up file based variables 00:01 ERROR: Job failed: exit code 1 经过了一番瞎折腾以后,我依然没有解决问题,而每次push都要等待gitlab的ci构建两三分钟,实在磨不动的我去看了jekyll-github-metadata的README,结合上文的报错,我一下子就看懂了。 jekyll-github-metadata可以通过github中的信息自动为jekyll提供site.github、site.title、site.description、site.url和site.baseurl。而由于我们在用的是Gitlab,所以jekyll-github-metadata就无法获取到这些信息,需要我们手动指定。报错中缺少的就是url 于是打开_config.yml,把url给补上,顺便把别的变量一同加上,如图:

2021/4/16
articleCard.readMore

我在Archlinux上的常用软件

最近基本固定了在Archlinux上的常用软件,也供各位参考一下。 我是KDE用户,所以KDE家的软件会用得比较多。 浏览器:Firefox,Chromium备用(主要是使用chromium的网页翻译功能,还有就是打开一些对Firefox不太友好的网站) 下载器:curl,wget,motrix 根据不同使用场景更换下载器 终端:konsole 输入法:fcitx5-chinese-addons 即时通讯:telegram,deepin-wine-tim,deepin-wine-wechat,electron-qq,wechat-uos,linuxqq 播放器:vlc 编辑器:nano,kate,visual-studio-code-bin,typora,wps 图形类:pinta,drawio-desktop-bin,imagemagick 文件管理器:dolphin 文件传输:sftp(命令行里的),filezilla 系统、网络工具:latte-dock-git,v2raya,htop,gtop

2021/4/16
articleCard.readMore

使用Chrome的同步api为chromium开启同步功能

今年两三月的时候,Google限制了chromium的同步api次数,导致各个发行版内置的chromium将不再能继续使用Google的数据同步功能。 今天在翻 archlinuxcn 的群组的时候翻到了一段脚本: https://gist.github.com/foutrelis/14e339596b89813aa9c37fd1b4e5d9d5 大意就是说,由于Archlinux特殊的chromium启动方式导致我们可以在设置oauth2-client-id和oauth2-client-secret的情况下通过chrome的同步api继续使用Google的同步服务,说得太多了也没必要,毕竟原文就在那里,看不看取决于你,我这里直接给命令吧。 echo "--oauth2-client-id=77185425430.apps.googleusercontent.com --oauth2-client-secret=OTJgUOQcT7lO7GsGZq2G4IlT" >> ~/.config/chromium-flags.conf 再次打开chromium,你就会发现你心心念念的同步功能回来了。 然而,并不是所有的发行版都像 Archlinux 这样考虑到 oauth,我们也不可能像 Archlinux 官方那样有这个闲情雅致为没一个 Chromium 去添加这个 patch 以后重新编译一遍,大部分人都是直接用发行版源里的。针对这种情况,我们可以直接手写一个脚本 #!/usr/bin/bash export GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com export GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT exec /usr/bin/chromium-browser "$@" # 我用的 Fedora 的启动命令是 chromium-browser,别的发行版用户还请自行调整 当我满心欢喜地把脚本扔进 $HOME/.local/bin 后,我却突然发现 Fedora 官方源中把 chromium 的启动命令写死在了 /usr/bin/chromium-browser,如果直接去改 /usr/bin/chromium-browser 的话,每次更新都会被覆盖。 正确的做法应该是把 desktop 文件复制一份到桌面,再去改内容。 mkdir -p $HOME/.local/share/applications/ cp /usr/share/applications/chromium-browser.desktop $HOME/.local/share/applications/ sed -i "s|/usr/bin/chromium-browser|GOOGLE_DEFAULT_CLIENT_ID=77185425430.apps.googleusercontent.com GOOGLE_DEFAULT_CLIENT_SECRET=OTJgUOQcT7lO7GsGZq2G4IlT /usr/bin/chromium-browser|g" $HOME/.local/share/applications/chromium-browser.desktop

2021/4/15
articleCard.readMore

Appimage的文件储存在哪里

我不饿: 有人知道怎么删除appimage的用户数据吗? liolok | 李皓奇: 还是可以在用户的家目录下面乱写的吧 Lipis Apple: 不太讲武德:~/.local/share/(app) 算讲武德:~/.config/(app) 不讲武德:~/.(app)

2021/4/13
articleCard.readMore

使用Motrix接管Firefox的下载

本文是一篇个人笔记,不具有太强的技术性,仅仅是为后来者指个方向。 熟悉我的人都知道,我是一个Firefox的忠实用户,原因有二: ​ 一/ Firefox国际版同步功能国内可用 ​ 二/ moz://a(Firefox用户应该能在地址栏直接访问这个链接) 但是Motrix没有推出适用于Firefox的接管浏览器下载功能的插件,于是只能用aria2的插件。这个插件内置了AriaNG,对于aria2用户来说会比较实用,但是对于Motrix用户而言其实功能有些多余且不兼容,比如什么自动启动aria2什么的是无法实现的。 主要的配置过程我就图解了,退出前记得保存配置。

2021/4/11
articleCard.readMore

yay进阶

yay是一个AUR Helper,他可以执行pacman的几乎所有操作,并在此基础上添加了很多额外用法。 我没有在网络上查找到关于yay的、除了pacman基础用法和安装AUR包以外的中文教程,英文的也几乎没有看到,这也是我写这篇文章的原因所在。 本文通篇详讲yay的每一个设置/选项(大概就是archwiki那种干涩的行文思路),最后会给出我自己的一些常用命令,但不会做解释。 写作时参考了yay的英文使用手册,如果你的arch安装了yay,那么即可通过man yay命令随时查阅它。 Tips1: 本文中出现的foo一般是指包名,标注*的表示该选项默认启用。 Tips2: 使用电脑端的访客可以在侧栏以获取目录。 基本用法 yay的基本用法是yay <operation> [options] [targets]、yay foo和yay,yay <operation> [options] [targets]的用法可以讨论的点比较多,我会在后文中一一道来。 yay 当我们仅执行yay,后面不跟任何参数时,yay会执行操作yay -Syu,他会先调用pacman更新源的数据库、更新所有从源内安装的软件包,并检查你的AUR包有没有更新。 yay foo 通过yay后面直接跟包名的命令会让yay直接在源和AUR内搜索带有foo关键词的包(包名和简介中只要出现foo都会被一网打尽),以下是我执行yay dingtalk的输出 5 aur/com.dingtalk.deepin 5.0.15deepin7-1 (+0 0.00) Deepin Wine dingtalk 4 aur/deepin.com.dingtalk.com 5.1.28.12-2 (+1 0.12) DingTalk Client on Deepin Wine 3 aur/dingtalk 2.1.3-1 (+3 0.00) 钉钉桌面版,基于electron和钉钉网页版开发,支持Windows、Linux和macOS 2 aur/dingtalk-linux 3.5.5-1 (+6 0.12) May be the official Linux experimental version 1 aur/dingtalk-electron 2.1.9-1 (+9 0.15) 钉钉Linux版本 ==> Packages to install (eg: 1 2 3, 1-3 or ^4) == 输入每一项对应的序号即可进入相应的安装过程。 yay <operation> [options] [targets] 在这里,<operation>每次只能有一个,[options]和[targets]可以有多个,且多个[options]可以合起来写在一起。比如yay -P -s -f可以直接写成yay -Psf,顺序也可以颠倒,-Psf和-sPf没区别。 -Y (--yay) -Y行为其实是yay的默认行为,当你没有加其他的行为参数时,yay就会执行-Y参数,可以跟--gendb和-c。 --gendb 生成AUR数据库。仅当从另一个AUR Helper迁移到yay时,才应使用此选项。(根据我的个人理解,是根据你Arch内安装的源内找不到的包的包名去AUR里寻找对应的PKGBUILD,并且把能找到的PKGBUILD给clone到~/.cache/yay/目录下) 千玄子大佬说:“简单说来就是把在 AUR 的 PKGBUILD 下下来然后比对是否要更新。” -c(--clean) 清除不再需要的、没有被依赖的包。(相当于apt中的autoremove) -P(--show) 执行特定的Print操作。可以跟的[option]有-c、-f、-d、-g、-n、-s、-u、-w、-q -c(--complete) Print所有源内和AUR软件包的列表。这是给命令行操作提供的,并不打算由用户直接使用。(意思是启用了这个选项以后你的终端会出现一大串长常的列表来告诉你你的Arch到底可以从哪里安装哪些包,并不是直接给你用的,是作为数据留给别的命令来玩耍的) -f(--fish) 在输出结果到终端时,会专门为fish用户做微调。(但是根据SamLukeYes大佬说他用fish体验下来并没有感知到加不加有什么区别,应该是属于感知不强的选项) -d(--defaultconfig) Print默认的yay配置。 -g(--currentconfig) Print当前的yay配置。 -n(--numberupgrades) 数一数你现在还有多少AUR包待更新。yay作者不推荐你使用呢,他推荐你用yay -Qu或者wc -l来代替它。 -s(--stats) 会展示一大堆信息,如下 [zhullyb@Archlinux ~]$ yay -Ps ==> Yay version v10.2.0 #yay版本 =========================================== ==> Total installed packages: 1240 #总共安装了多少包 ==> Total foreign installed packages: 24 #多少包不是从源里安装的 ==> Explicitly installed packages: 271 #有多少包是你自己主动安装的(而不是作为依赖安装的) ==> Total Size occupied by packages: 14.3 GiB #安装的所有包合在一起一共占了你多少空间 =========================================== ==> Ten biggest packages: #十个体积最大的包 wps-office-cn: 990.9 MiB ttf-sarasa-gothic: 855.5 MiB linux-firmware: 652.3 MiB baidunetdisk-bin: 494.7 MiB com.antutu.benchmark: 412.0 MiB wine: 402.2 MiB linux-xanmod-cacule-uksm-cjktty: 324.4 MiB microsoft-edge-dev-bin: 316.4 MiB wine-mono: 316.2 MiB deepin-wine5-i386: 259.5 MiB =========================================== :: Querying AUR... -> Missing AUR Packages: zhullyb-archlinux-git #AUR里找不到的包 -> Flagged Out Of Date AUR Packages: xml2 #AUR中被人标注过期的包 -u(--upgrades) 展示你所有待更新的包。 -w(--news) 展示来自archlinux.org的新闻。需要注意的是,这里的新闻是具有时效性的,只有在你的Arch最后一次更新以后发出来的新闻才会被显示出来。如果你不想要yay判断新闻时效性,你可以通过yay -Pww(即两个w)来获取所有能获得的新闻。 -q(--quiet) 在输出新闻的时候,仅输出新闻的标题。该功能需要与-w连用,即yay -Pwq。 -G(--getpkgbuild) 后跟包名。需要注意的是,如果指定的包不存在于官方源,则无法输出,后跟-f、-p参数。 如果希望仅获取来自AUR(即排除第三方源的干扰)的PKGBUILD,后需跟-a参数。 -f(--force) 强制下载AUR中的PKGBUILD,如果它在yay缓存目录已经存在了,那就覆盖它! -p(--print) Print指定包的PKGBUILD。 pacman 拓展用法 yay虽然可以使用pacman的所有<operation>,但是它远不仅于此。在这一段,我将向你介绍yay中包含的那些pacman不包括的pacman <operation -S -S, -Si, -Sl, -Ss, -Su, -Sc, -Qu 这些操作pacman都支持,而与pacman不同的是,yay的这些操作可以涵盖到官方源/第三方源和AUR中的所有包。 -Sc yay将会清除AUR包构建时的缓存和没有被track的文件。没有被track的文件在这里指AUR包构建时下载的sources或者构建完成的pkg包,但是vcs sources会被保留(比如.git文件夹) 全局的[options] 全局是指在所有<operation>下都可以加啦。 --repo 假定你给出的包名只存在源里(忽视AUR的存在) -a(--aur) 假定你给出的包名只存在AUR中(忽视源的存在) 配置设置 原版的man手册排的比较混乱,我这里自己细分了几个类型,或许不是特别专业,但我希望能够帮助你们理解。 自定义调用命令型 --editor <command> 设置编辑时调用的编辑器。 --makepkg <command> 设置makepkg时需要调用makepkg命令(一般情况下用不到) --pacman <command> 设置运行pacman时需要调用pacman命令(一般情况下用不到) --tar <command> 设置makepkg解压tar资源时调用的tar命令(一般情况下用不到) --git <command> 设置makepkg clone git资源时调用的git命令(比如你可以安装AUR中的fgit-go,使用--git fgit参数来让fastgit代理clone的过程) --gpg <command> 设置gpg验证资源时调用的gpg命令 --sudo <command> 设置调用sudo获取su权限安装pkg时所调用的sudo命令。 自定义配置文件型 --config <file> 设置读取的pacman配置文件。 --makepkgconf <file> 设置读取的makepkg配置文件。 --nomakepkgconf 不读取系统中的makepkg.conf,仅使用Arch默认状态下的配置文件。 自定义路径类型 --builddir <dir> 设置build路径,默认路径为~/.cache/yay/ --absdir <dir> 设置abs路径,默认路径为~/.cache/yay/abs/ 参数传递型 --editorflags <flags> 后跟需要跟随传递给编辑器的参数。如果需要传递多个参数,可以使用引号。 --mflags <flags> 后跟需要跟随传递给makepkg的参数。如果需要传递多个参数,可以使用引号。 这个用的人不多,但其实是非常好用的一个功能。在我们安装deepin-wine-tim等包的时候,很可能会遇到文件明明完整但checksum不通过的情况,这时我们可以跟一个--skipchecksums参数传递给makepkg以跳过checksum的过程。 --gpgflags <flags> 后跟需要跟随传递给pgp的参数。如果需要传递多个参数,可以使用引号。 --sudoflags <flags> 后跟需要跟随传递给sudo的参数。如果需要传递多个参数,可以使用引号。 菜单配置型 clean菜单 *--cleanmenu 启用清除询问菜单。(询问你是否需要清除已存在的文件) --nocleanmenu 禁用清除询问菜单。(不询问你是否需要清除已存在的文件) --answerclean 自动回答cleanmenu,后跟<All|None|Installed|NotInstalled>参数。 *--noanswerclean 不设置自动回答。 diff菜单 *--diffmenu 启用对比询问菜单。(询问你是否需要对比本地文件和AUR文件) --nodiffmenu 禁用对比询问菜单。(不询问你是否需要对比本地文件和AUR文件) --answerdiff 自动回答cleanmenu,后跟<All|None|Installed|NotInstalled>参数。 *--noanswerdiff 不设置自动回答。 edit菜单 --editmenu 启用修改询问菜单。(询问你是否需要修改PKGBUILD以及相关文件) *--noeditmenu 禁用修改询问菜单。(不询问你是否需要修改PKGBUILD以及相关文件) --answeredit 自动回答editmenu,后跟<All|None|Installed|NotInstalled>参数。 *--noansweredit 不设置自动回答。 upgrade菜单 *--upgrademenu 启用更新询问菜单。(询问你是否需要更新AUR包) --noupgrademenu 禁用更新询问菜单。(不询问你是否需要更新AUR包) --answerupgrade 自动回答upgrademenu,后跟<All|None|Installed|NotInstalled>参数。 *--noanswerupgrade 不设置自动回答。 removemake菜单 *--askremovemake 在编译结束后,询问是否删除make depend。 --removemake 在编译结束后,删除make depend。 --noremovemake 在编译结束后,不删除make depend。 provides菜单 *--provides 搜索AUR包时,一同寻找其在AUR上的依赖程序。 当找到多个提供该依赖的包时,将出现一个菜单,提示您选择一个。尽管这不会引起注意,但这会增加依赖项解决时间。 --noprovides 搜索AUR包时,不在AUR上寻找其依赖程序。尽管yay不会再次弹出依赖菜单供你选择,yay调用pacman时依然会出现pacman的选择菜单让你选择。 pgpfetch菜单 *--pgpfetch 询问你是否从每个PKGBUILD的validpgpkeys字段导入未知的PGP密钥。 --nopgpfetch 不自动导入陌生的PGP密钥。 useask选项 *--useask 调用pacman的--ask询问用户是否删除系统中与当前包冲突的软件包。 --nouseask 不调用pacman的--ask询问用户是否删除系统中与当前包冲突的软件包,遇到冲突的软件包时直接报错,由用户来手动解决。 combinedupgrade菜单 --combinedupgrade 在系统更新期间,将源内包和AUR包的更新菜单合并到一起。 *--nocombinedupgrade 在系统更新期间,先支持源内包的升级,完成后再进行AUR包的升级。 T or F 型 devel --devel 在系统更新期间,检查AUR的vcs包是否有更新,当前仅支持AUR的-git包。 devel查询是使用git ls-remote对比安装时和现在最新的commit_id完成的。 *--nodevel 在系统更新期间, 不检查AUR的vcs包是否有更新。 timeupdate --timeupdate 在系统更新期间,将已安装软件包的构建时间与每个软件包的AUR的最后修改时间进行比较。 *--notimeupdate 在系统更新期间,不将已安装软件包的构建时间与每个软件包的AUR的最后修改时间进行比较。 redownload --redownload 就算PKGBUILD已经存在,也要重新从AUR上获取一份新的PKGBUILD并覆盖原有PKGBUILD。 --redownloadall 就算PKGBUILD已经存在,也要重新从AUR上获取所有AUR包的PKGBUILD并覆盖原有PKGBUILD。 *--noredownload 当下载PKGBUILD时,,如果发现cache中的PKGBUILD版本>=AUR上的版本时,直接使用本地的PKGBUILD。 rebuild --rebuild 即使在cache中有可用的二进制包的情况下,也始终要重新编译目标软件包。 --rebuildall 即使在cache中有可用的二进制包的情况下,也始终要重新编译所有的AUR包。 --rebuildtree 安装AUR包时,以递归方式重新编译并重新安装其所有AUR依赖包,即使已安装的依赖项也是如此。 该选项使您可以轻松地针对当前系统的库重新构建软件包,如果它们变得不兼容。(比如python3.8->3.9) *--norebuild 构建软件包时,如果在缓存中找到该软件包并且该软件包与想要的软件包的版本相同,则跳过软件包的编译过程并使用现有的二进制程序。 sudoloop --sudoloop 在后台循环调用sudo,以防止sudo授权在长时间构建期间超时。 *--nosudoloop 不在后台循环调用sudo,可能会导致sudo授权在长时间构建期间超时。 batchinstall --batchinstall 在构建和安装AUR包时,对每个软件包的安装进行排序,而并非在构建之后立刻安装每个软件包时。 需要注意的是,一旦构建了所有软件包,或者需要构建队列中的软件包作为构建另一个软件包的依赖项,应当在安装队列中安装所有软件包。 *--nobatchinstall 在构建AUR包成功后立即安装。 clearafter --cleanafter 在构建AUR包完成以后清除cache文件。 *--nocleanafter 在构建AUR包完成以后不清除cache文件。 其他型 --save 把你这一次执行yay后面跟的配置参数永久保存下来。 --aururl 更改aur源地址(默认为 https://aur.archlinux.org ),适用于中国用户,可以使用此参数将AUR的地址设置成清华的反代,具体的配置命令为 yay --aururl "https://aur.tuna.tsinghua.edu.cn" --save TUNA 的反代已经取消,可以使用如下命令设置回 AUR 官方源 yay --aururl "https://aur.archlinux.org" --save --sortby 在搜索过程中,按特定条件对AUR结果进行排序,后跟<votes|popularity|id|baseid|name|base|submitted|modified参数,默认为votes。 --searchby 通过指定查询类型来搜索AUR软件包,后跟<name|name-desc|maintainer|depends|checkdepends|makedepends|optdepends参数,默认为name-desc。 *--topdown 优先展示源内包,其次才是AUR包 --bottomup 优先展示AUR包,其次才是源内包 --requestsplitn <number> 设置在每次向AUR的请求的最大数值(默认150)。数值越高,请求时间越短,但是单次请求的数值过大会导致error。当这个数值>500时你应当特别注意这一点。 --completioninterval <days> 刷新完成高速缓存的时间(以天为单位,默认为7)。 将此值设置为0将导致每次刷新缓存,而将其设置为-1将导致永远不刷新缓存。 我个人的常用命令 yay yay foo yay -Sa foo yay -Scc yay -Ps yay -Pww yay -Gpa yay -Ga 本文同时发布于「知乎专栏」,如果你恰好有知乎帐号的话或许可以考虑帮我点个赞?

2021/4/4
articleCard.readMore

抛弃DisplayManager,拥抱startx

在正常情况下,我们会给Linux装上一个DisplayManager以方便我们输入账号密码来进入图形化系统,但是我不想要额外装一个DM来启动我的图形化系统(而且之前我一直用的sddm也出过一小阵子的问题) 首先卸载我的sddm sudo pacman -Rsnc sddm sddm-kcm 安装startx所在的软件包 sudo pacman -S xorg-xinit 从/etc/X11/xinit/xinitrc拷贝一份.xinitrc cp /etc/X11/xinit/xinitrc ~/.xinitrc 注释掉最后5行 #twm & #xclock -geometry 50x50-1+1 & #xterm -geometry 80x50+494+51 & #xterm -geometry 80x20+494-0 & #exec xterm -geometry 80x66+0+0 -name login 然后需要在结尾处写上我们的配置。我用的桌面是Plasma,查询wiki To start Plasma with xinit/startx, append export DESKTOP_SESSION=plasma and exec startplasma-x11 to your .xinitrc file. If you want to start Xorg at login, please see Start X at login. 然后在xinitrx文件末尾处写上我们的配置 export DESKTOP_SESSION=plasma startplasma-x11 Ps: 在第二行中,wiki中让我们使用exec,代表当xorg桌面会话结束后自动退出当前用户,而我不想退出,所以没加 至此,我们的startx就已经配置完成了,重启后只需要在tty界面登录用户后输入startx并回车即可进入图形化界面。

2021/3/14
articleCard.readMore

FireFox? IceDoge!!!

事情的起因是这样的。 Solidot Mozilla 强调 Firefox 的 logo 仍然包含小狐狸 2021-02-27 20:02 #Firefox 过去几天一个广泛流传的 meme 宣称,Firefox 著名的红色小狐狸 logo 正被逐渐简化直至消失。Mozilla 官方博客对此做出了回应,强调 Firefox 的 logo 将会始终包含小狐狸,他们没有消除狐狸的计划。作为反击这一 meme 行动的一部分,Mozilla 修改了Firefox Nightly 的 logo,将著名的网络 meme 狗币中的柴犬图像与红色小狐狸 logo 整合在一起。如果你下载安装 Nightly 版本,你会看到狗狗在看着你。 这只狗是非常可爱,大概是长下图这个样子。 但是我是一个将Firefox当成主浏览器的用户,咱不可能去用Nightly,所以我就打算把我这里这只稳定版的红色小狐狸 换成上面的那只狗。 通过直接写入用户目录下的icon可以在不覆盖浏览器原图标、不给包管理器惹麻烦的情况下实现我们的目标,所以,代码如下 #/usr/bin/sh # This script will change icon of you Firefox Browser into a bluedoge # Depend on imagemagick cd ~ curl https://www.mozilla.org/media/img/logos/firefox/logo-nightdoge-lg-high-res.14f40a7985fe.png > logo-nightdoge-lg-high-res.14f40a7985fe.png for _resolution in 16 22 24 32 48 64 128 192 256 384 do mkdir -p ~/.local/share/icons/hicolor/${_resolution}x${_resolution}/apps/ convert -resize "${_resolution}x${_resolution}" "logo-nightdoge-lg-high-res.14f40a7985fe.png" "firefox${_resolution}.png" mv "firefox${_resolution}.png" ~/.local/share/icons/hicolor/${_resolution}x${_resolution}/apps/firefox.png done rm logo-nightdoge-lg-high-res.14f40a7985fe.png # If you want to change back your icons, run the command bellow # rm ~/.local/share/icons/hicolor/*/apps/firefox.png

2021/2/27
articleCard.readMore

在Archlinux上解包A/B机型的payload.bin

解包A/B机型的OTA更新包时,会发现zip文件中只有一个payload.bin文件 解包这个文件,我们需要用到这个叫payload_dumper的python脚本,同时需要安装依赖: community/python-google-api-core和python-bsdiff4,我解包的时候发现缺少python3版本的python-bsdiff4,因此已经打包上传至AUR git clone https://github.com/vm03/payload_dumper.git cd payload_dumper mv path/to/payload.bin payload_dumper python payload_dumper.py payload.bin 然后就可以在该项目文件夹的output路径下找到解包后的img镜像

2021/2/7
articleCard.readMore

如何解决adb未授权的问题

在调试安卓设备的时候,我们经常会遇到adb未授权的问题,本方案适用于未开机时遇到以下两种情况。 当我们编译eng的时候,adb应该会默认授权所有设备,但是有部分Rom并不会。 当我们编译userdebug的时候,adb就不会授权给所有设备了,如果卡开机,使用adb抓取log将会是非常麻烦的事情。 此时我们需要手动导入我们的adbkey 手机重启到Recovery模式 找到你电脑的adbkey公钥,一般叫做adbkey.pub adb push ${the/location/to/your/key} /data/misc/adb/adb_keys 比如我就是 adb push ~/.android/adbkey.pub /data/misc/adb/adb_keys 重启手机,愉快苦逼地去抓log

2021/1/25
articleCard.readMore

虚拟Python环境

在python使用中,我们经常会遇到本地默认python版本与程序所需要的python版本不一致的问题,此时我们需要创建一个虚拟的python环境。 安装目标python版本 Ubuntu系 主程序 参考https://www.cnblogs.com/m3721w/articles/10344887.html pip sudo apt isntall python-pip #python2 sudo apt isntall python3-pip #python3 Archlinux yay -S python【xx】 #如yay -S python38 源码安装 主程序: wget https://www.python.org/ftp/python/【x.x.x】/Python-【x.x.x】.tgz tar xzvf Python-【x.x.x】.tgz cd Python-x.x.x ./configure make sudo make install pip curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py sudo python【x.x】 get-pip.py -i https://mirrors.bfsu.edu.cn/pypi/web/simple pip config set global.index-url https://mirrors.bfsu.edu.cn/pypi/web/simple #换源 安装virtualenv 常规发行版 pip install virtualenv #python2 pip3 install virtualenv #python3 Archlinux sudo pacman -S python2-virtualenv #python2 sudo pacman -S python-virtualenv #python3 使用virtualenv 创建virtualenv环境 常规发行版 virtualenv $(TRAGET_PATH) python=python【x.x】 Archlinux virtualenv2 $(TRAGET_PATH) python=python2.【x】 #python2 virtualenv $(TRAGET_PATH) python=python3.【x】 #python3 启用virtualenv环境 source $(TARGET_PATH)/bin/activate 退出virtualenv环境 deactivate 删除virtualenv环境 rm -rf $(TRAGET_PATH)

2021/1/20
articleCard.readMore

为什么我不推荐Manjaro

说起Linux发行版,很多人都会去推荐Manjaro给新手使用,原因很简单——安装简单、有庞大的AUR和ArchlinuxCN提供软件、有丰富的ArchWiki以供新手查阅。那么,为什么大多数Archlinux用户(包括我)始终不推荐Manjaro作为自己使用的发行版呢。 首先来了解一下两款Linux发行版 Archlinux Archlinux是一款滚动发行版,所有的软件全部都基于上游最新的源代码进行编译,源内也仅仅保留最新版本,是最为激进的发行版之一,甚至或许没有之一。 Manjaro Manjaro是一款基于Archlinux的滚动发行版,部分软件同样基于上游源代码编译,同时也有部分软件包直接从Archlinux源内直接拿二进制包。与Archlinux不同的是,Manjaro大部分软件更新相比Archlinux会滞后一个星期,一些比较重要的软件甚至会滞后两个星期以上(比如Python3.9就滞后了19天)以保证稳定性。(虽然我目前观察下来这个稳定性就是出现Bug和修复Bug都比Archlinux慢一个礼拜) 接下来就是正文 Archlinux 和 Manjaro 都不适合Linux小白 Archlinux和Manjaro都是激进的滚动发行版,作为一个滚动发行版都会有滚坏的风险,这就要求用户有一定的Linux使用基础,能够多关注更新动态,在系统罢工后有修复系统的能力,因此我不会给小白推荐Archlinux/Manjaro这样的发行版(虽说能够用纯cli界面安装Archlinux的用户其实已经有一定的水平了)。 ArchWiki 不是 ManjaroWiki Manjaro官方为了最大限度地降低用户的使用门槛,为用户打造了一套开箱即用的环境,这听起来很好。 但是Manjaro官方为了降低用户使用门槛,不得不替用户去做一些选择,写上一些默认配置,在必要的地方对系统进行魔改。因此,ArchWiki上面的解决方案并非在Manjaro上能够100%适用,因此不要指望在系统使用过程中ArchWiki能够解决你所有的问题,有相当一部分问题你需要去查阅纯英文版的ManjaroWiki。 AUR(Archlinux User Repository)&ArchlinuxCN 并不是为 Manjaro 准备的 AUR和ArchlinuxCN源都是Archlinux用户为Archlinux打包的常用软件,因此所有的软件都是选择Archlinux最新的软件作为依赖来编译/打包的。上文中我们提到过,Manjaro源内的软件会滞后更新。因此AUR和ArchlinuxCN内一些对于依赖版本要求比较苛刻的软件会在Manjaro这个更新比较落后的发行版上不工作。 我知道这听起来会有些荒唐,不过我可以举出一个就发生在不久之前的生动的例子。 Archlinux在2020年10月17日将grpc从1.30更新到了1.32,qv2ray开发者反应迅速,在几个小时内直接更新了基于grpc-1.32的qv2ray,接着是仍然在使用grpc-1.30的Manjaro用户的一片哀嚎。。。 解决方法有很多,比如临时使用Archlinux源把grpc更新到1.32、通过AppImage安装qv2ray等等,但是如你所见,Manjaro用户使用AUR&ArchlinuxCN确实容易出现问题。 附:AUR上需要下载源码的自己编译的包不会碰到依赖的版本问题,但是仍然有部分情况下PKGBUILD会直接因为依赖版本号被写死而编译出错。而ArchlinuxCN清一色是编译好的二进制包,所以Manjaro用户使用ArchlinuxCN相比AUR出问题的几率更加大一点。 此外,他们延迟两周,并不是在测试 Arch 包打包本身的质量,而是在测试他们拿来 Arch 的包和他们自己乱改的核心包之间的兼容性。以下内容来自于一位 Archlinux Trusted User manjaro 這個分三個 channel 延遲兩週的做法,原因出於兩點他們處理打包方面非常存疑的做法 他們想要自己打包一部分非常核心的包,包括 glibc 內核 驅動 systemd 他們不想重新打整個發行版所有包,想直接從 Arch 拿二進制來用。 這兩個做法單獨只做一個沒啥事,放一起做就很容易導致他們自己打包的核心包破壞了二進制兼容,以至於他們從 Arch 拿的二進制包壞掉。所以他們延遲兩週,並不是在測試 Arch 包打包本身的質量,而是在測試他們拿來 Arch 的包和他們自己亂改的核心包之間的兼容性。Arch 本身有一套機制保證 Arch 打包放出來的時候是測試好相互兼容的,被他們替換掉幾個核心包之後就不一定兼容了,他們也沒有渠道涉足 Arch 內部打包機制,從 Arch 組織內部了解什麼時候放出包之類的信息。綜合這些情況,對他們來說合理的做法就是延遲一陣子讓他們自己的人測試一下。 所以作為證據你看他們的打包者開發者很少會向 Arch 上游反饋測試打包遇到的問題…因為 manjaro unstable 和 manjaro testing 會遇到的問題大部分都是他們自己造成的問題而不是 Arch 的問題。 要是他們誠實地把這個情況傳達給他們用戶的話我不責怪他們。Arch整個滾動發布的生態也不利於下游發行版。Debian 這種上游打包時可以約定版本兼容性的範圍,可以鎖 abi ,Arch 打包本身就不考慮這些,作為Arch下游就的確很難操作。我反感 manjaro 的點在於他們把這種難看的做法宣傳成他們的優勢,還為了這個看起來是優勢故意去抹黑 Arch 作為上游的打包質量…做法就很難看了。 —— farseerfc Manjaro 没有 Archive 源 Archlinux拥有一个archive源,通过Archive源,你可以将你的系统滚到任何一天的状态,比如在你不知道更新了什么滚炸了以后,你可以用Archive源回滚到三天前的状态,等bug修复完以后再用回正常的Archlinux源。况且,这个Archive源在国内拥有tuna和bfsu两个镜像源(虽然这两个镜像源并不是完整的镜像,而是每隔7天镜像一次),不会存在访问速度过慢的状况。有名的downgrade软件也是基于Archive源使用的。而Manjaro?很遗憾,没有。 写在最后 如果你有一定的Linux基础,阅读了我上面的科普以后仍然要去使用Manjaro也没有关系,但是记得遵守以下几点以确保你在Arch社区不会被打死。 谨慎使用AUR和ArchlinuxCN 使用AUR和ArchlinuxCN时遇到问题请不要反馈 在Arch社区提问时请提前说明自己在使用Manjaro 不要根据Manjaro的使用经验随意编辑ArchWiki

2021/1/1
articleCard.readMore

UOS到底有没有Secure Boot签名/UOS引导怎么修复

以下内容来自2020年12月22日晚上的大佬对话,非本人原创。 吃瓜群众: 话说UOS到底有没有Secure boot签名啊 某dalao: 用的是ubuntu的 吃瓜群众: 哪来的签名? 某dalao: 这就不得不讲到另一个槽点了s 吃瓜群众: ubuntu给他们签? 某dalao: 不不不,用的是ubuntu签好名的那个binary 然后ubuntu的那个binary会在EFI分区的ubuntu目录找配置 于是他们在安装器里写了个逻辑 把deepin目录的内容复制一份到ubuntu目录 (而不是patch grub包,或者写在grub包的postinst之类的地方) 后果是用户只要搞坏了引导 用网上任何教程都恢复不了 因为没人会教你建一个ubuntu目录,然后把deepin目录的内容复制进去) 如果不做这一步,任你怎么grub-install啊,update-grub啊,引导就还是坏的

2020/12/22
articleCard.readMore

No Hello

Don't Just Say "Hello" in Chat. 别在向别人问问题的时候问“在吗?” 英文原版请查阅这里,此处是我个人的翻译版。 2010-07-19 12:32:12 你: 在吗? 2010-07-19 12:32:15 我: 在的。 ## 我就这这里静静的等待你打字描述自己的问题 2010-07-19 12:34:01 你: 我正在进行 [莫些事情] 然后我正尝试 [等等。。。] 2010-07-19 12:35:21 我: 这样啊,你应该 [我的回答] 这就像是你在和某人打电话,你接起电话说了一声:”喂?“,然后放下手机打开免提等待着对方的提问,这很低效 请使用如下格式: 2010-07-19 12:32:12 你: 你好,我正在进行 [某些事情] 然后我正尝试 [等等。。。] 2010-07-19 12:33:32 我: [我的回答] 这样做的原因是:你可以更快速地获得你想要的答案,而不是让对方在那边傻傻地等待你以龟速打字。 你的潜意识里试图不打断对方的回应,得到对方的回复以后再回答以显示你的礼貌,正如你在给别人打电话时那样。但是,网络聊天并不是打电话,通常情况下,打字要比说话慢得多。你的行为不是在彰显自己的礼貌,而是在浪费对方的时间。 其他的用语比如“你好,你在吗?”,“老王,问你个很简单的问题。”,“你有空吗?”都是很愚蠢的行为,在网络聊天中直接问问题就好。 如果你觉得直接问问题不礼貌,你可以采用以下的格式: 2010-07-19 12:32:12 你: 你好,如果你不介意的话我想问个问题,我正在进行 [莫些事情] 然后我正尝试 [等等。。。] 这样提问的另一个好处是:你的提问题同时具有即时性和留言性。如果对方不在,而你在对方上线之前就离开了,他们仍然可以回答您的问题,而不仅仅是盯着你发的“在吗”并为你究竟想要问什么问题而好奇。 (如果你使用的聊天软件支持查看对方的在线状态或者对方是否看到消息,你要做好被对方忽视的准备)

2020/10/8
articleCard.readMore

安卓解包笔记

brotli -d system.new.dat.br sdat2img system.transfer.list system.new.dat mount system.img {known_path} Get brotli here & sdat2img here

2020/8/10
articleCard.readMore

国内Linux镜像源列表

企业镜像 阿里 https://developer.aliyun.com/mirror/ 腾讯 https://mirrors.cloud.tencent.com/ 网易 http://mirrors.163.com/ 华为 https://mirrors.huaweicloud.com/ 首都在线 http://mirrors.yun-idc.com/ 搜狐 http://mirrors.sohu.com/ 平安云 https://mirrors.pinganyun.com/ 高校镜像 清华大学 https://mirrors.tuna.tsinghua.edu.cn/ https://opentuna.cn/ farseerfc: tuna 有兩個服務器互相負載均衡,這倆服務器之間不同步,就偶爾遇到版本回退。 中国科技大学 http://mirrors.ustc.edu.cn/ 浙江大学 https://mirrors.zju.edu.cn/ 北京外国语大学 https://mirrors.bfsu.edu.cn/ 北京大学 https://mirrors.pku.edu.cn/Mirrors 北京交通大学 https://mirror.bjtu.edu.cn/ 北京理工大学 https://mirrors.bit.edu.cn/web/ 上海交通大学 https://mirrors.sjtug.sjtu.edu.cn/ 大连东软信息学院 http://mirrors.neusoft.edu.cn/ 兰州大学 http://mirror.lzu.edu.cn/ 南京大学 http://mirrors.nju.edu.cn/ 哈尔滨工业大学 https://mirrors.hit.edu.cn/ 南京邮电大学 https://mirrors.njupt.edu.cn/ 山东大学 https://mirrors.sdu.edu.cn/ 东北大学 http://mirror.neu.edu.cn/ 大连理工大学 http://mirror.dlut.edu.cn/ 南洋理工学院 https://mirror.nyist.edu.cn/ 南方科技大学 https://mirrors.sustech.edu.cn 重庆大学 http://mirrors.cqu.edu.cn/ 西北农林科技大学 https://mirrors.nwsuaf.edu.cn/ 山东女子学院 https://mirrors.sdwu.edu.cn/

2020/7/11
articleCard.readMore

git笔记

git自动填入账号密码 打开终端,输入 git config --global credential.helper store 此时,我们就已经开启了git账号密码的本地储存,在下一次push时只要输入账号密码就可以一劳永逸了。 git设置默认的commit编辑器 git config --global core.editor $editor_name Ps: $editor_name指的是你选用的编辑器,一般为nano、vim等 pick一个仓库中连续的几个commit git cherry-pick <commit1_id>..^<cimmitn_id> Ps: <commit1_id>和<commitn_id>分别指第一个你想要pick的commit_id和最后一个你想要pick的commit_id pick失败时如何撤销此次pick git cherry-pick --abort 踩坑记录 发生背景: clone了一个内核仓库,大概是1.4G左右的大小,在github新建了一个repository,打算push上去,报错如下 [zhullyb@Archlinux sdm845]$ git push -u origin master Enumerating objects: 5724101, done. Counting objects: 100% (5724101/5724101), done. Delta compression using up to 4 threads Compressing objects: 100% (983226/983226), done. Writing objects: 100% (5724101/5724101), 1.34 GiB | 2.46 MiB/s, done. Total 5724101 (delta 4693465), reused 5723950 (delta 4693375), pack-reused 0 error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: INTERNAL_ERROR (err 2) send-pack: unexpected disconnect while reading sideband packet fatal: the remote end hung up unexpectedly Everything up-to-date 搜索互联网,最终使用的解决方案 git config http.version HTTP/1.1 #原文中加了--global,不过我就临时遇到这种情况,不考虑加 最终应该可以使用如下命令设置回来 git config http.version HTTP/2

2020/7/11
articleCard.readMore

repo笔记

清除同步过程中产生的不完整碎片文件 在源码路径/.repo下搜索tmp_pack 将搜索结果中出现的所有文件全部删除 以下命令仅供参考 rm -rf */*/*/*/objects/pack/tmp_pack_* repo自动同步 下载脚本 echo #!/bin/bash echo "======start repo sync======" repo sync --force-sync --current-branch --no-tags --no-clone-bundle --optimized-fetch --prune -j$(nproc --all) while [ $? == 1 ]; do echo "======sync failed, re-sync again======" sleep 3 repo sync --force-sync --current-branch --no-tags --no-clone-bundle --optimized-fetch --prune -j$(nproc --all) done > repo.sh 授予运行权限 chmod a+x repo.sh 运行脚本 bash repo.sh

2020/7/11
articleCard.readMore

在Windows下给cmd设置代理

cmd打开方法 按住win+R键,调出一个运行框,接着输入cmd并回车即可 设置cmd代理 一般性使用的如果是ShadowsockR的话,代理端口都是1080,v2ray的话则是10808 ShadowsocksR set http_proxy=http://127.0.0.1:1080 set https_proxy=http://127.0.0.1:1080 v2ray set http_proxy=http://127.0.0.1:10808 set https_proxy=http://127.0.0.1:10808 为git设置代理 ShadowsocksR git config --global http.proxy http://127.0.0.1:1080 git config --global https.proxy http://127.0.0.1:1080 v2ray git config --global http.proxy http://127.0.0.1:10808 git config --global https.proxy http://127.0.0.1:10808

2020/3/3
articleCard.readMore