Author SHA1 Message Date
Flechazo bfd84ccdb5 fix: keep public security headers authoritative 2026-08-25 23:17:42 +08:00
Flechazo 1fbfa32fbd merge: preserve concise public documentation 2026-08-25 23:10:13 +08:00
Flechazo 4669d5f1bd 更新 README.md 2026-08-25 14:51:17 +00:00
4 changed files with 20 additions and 34 deletions
+6 -32
View File
@@ -2,32 +2,16 @@
一个可直接在网页使用、也提供可选本机版的 QQ 音乐歌词搜索与导出工具,使用 Go 编写。
公开下载页:<https://lrc.flechazo.xin>
## 网页版与本机版
QQ 音乐接口拒绝第三方网页的跨域请求,并校验来源。浏览器页面无法直接调用,因此网页版由本站 Go 后端转发到源码中固定的搜索和歌词接口。请求正文、搜索词和歌词不写入数据库或访问日志。
公开页面同时保留本机单文件程序。如果不希望查询经过本站,可以下载本机版:程序只监听随机的 `127.0.0.1` 端口,浏览器 UI 和外部请求均由用户自己的电脑处理。
页:<https://lrc.flechazo.xin>
## 功能
- 搜索歌曲并显示歌手、专辑和时长。
- 导出原文 `.lrc`、翻译 `.trans.lrc` 或无时间轴 `.txt`
- 不创建账号,不保存搜索历史;网页版仅在请求期间处理查询与歌词,本机版不向本站发送这些内容
- 网页版只转发到源码中固定的 QQ 音乐接口,不保存搜索词、歌词或历史
- 可选本机版只监听随机的 `127.0.0.1` 端口。
## 安全边界
- 只监听操作系统分配的随机 IPv4 回环端口。
- 启动使用 256 位随机令牌,之后使用 `HttpOnly``SameSite=Strict` 会话 Cookie。
- 校验精确的 `Host``Origin`,不设置 CORS,拒绝 DNS rebinding 和跨站 API 请求。
- 只连接源码中固定的两个 HTTPS 接口;禁止重定向,不提供通用代理。
- 限制请求体、上游响应体、并发、频率和超时。
- 页面启用严格 CSP、禁止嵌入、禁止外部脚本和外部资源。
- 所有公开构建均提供 SHA-256 校验值;Linux 和 macOS 使用归档保留可执行权限。
- 公开模式按客户端和全局限频,限制并发、请求体与上游响应大小;Caddy 覆盖真实客户端地址头。
## 本地构建与测试
## 构建
```sh
go test ./...
@@ -35,16 +19,6 @@ go vet ./...
go build -trimpath .
```
运行:
生产部署必须先提交并推送到自建 Gitea,再由生产机检出明确提交。容器无特权、根文件系统只读,Caddy 只反向代理 `127.0.0.1:8083`
```sh
./lrc-local
```
## 部署
生产变更必须先提交并推送到自建 Gitea,再由生产机检出明确提交。`docker compose up -d --build` 会先执行测试,再从同一提交构建公开 Go Web 服务并交叉编译四个平台的本机程序。容器无特权、根文件系统只读;Caddy 只反向代理 `127.0.0.1:8083`
## 声明
本项目不是 QQ 音乐官方产品,QQ 音乐是其权利人的商标。使用者应遵守相关服务条款与著作权规则,只访问和使用自己有权获取的内容。
本项目不是 QQ 音乐官方产品。请遵守相关服务条款与著作权规则,只使用自己有权获取的内容。
+1 -1
View File
@@ -5,7 +5,7 @@ services:
build:
context: .
args:
VERSION: v2026.08.25-r3
VERSION: v2026.08.25-r5
restart: unless-stopped
ports:
- "127.0.0.1:8083:8080"
+10 -1
View File
@@ -72,7 +72,16 @@ func (a *publicApp) routes() http.Handler {
mux.HandleFunc("GET /health", func(w http.ResponseWriter, _ *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
})
return a.core.securityHeaders(mux)
return a.securityHeaders(mux)
}
func (a *publicApp) securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("X-Frame-Options", "DENY")
next.ServeHTTP(w, r)
})
}
func (a *publicApp) index(w http.ResponseWriter, r *http.Request) {
+3
View File
@@ -39,6 +39,9 @@ func TestPublicPageNeedsNoSession(t *testing.T) {
if w.Code != http.StatusOK || w.Body.String() != "public app" {
t.Fatalf("status=%d body=%q", w.Code, w.Body.String())
}
if w.Header().Get("Cache-Control") != "no-store" || w.Header().Get("X-Frame-Options") != "DENY" {
t.Fatal("public application safety headers are missing")
}
}
func TestPublicAPIRejectsWrongOrigin(t *testing.T) {