- 系统候选补上 glyf 轮廓的 CJK 字体(文泉驿正黑/微米黑、DroidSansFallback、 AR PL UMing),并把 Noto Sans CJK 压到最后:它是 CFF/OTTO,freetype 必定解析失败, 排前面只会白白多读十几 MB - static/fonts 同时按当前工作目录与可执行文件目录查找,systemd 没配 WorkingDirectory 时也能找到(原先只按 CWD,文件在却报「没有可用字体」) - 启动时探一次字体:命中打 Info 带路径,缺失打 WARN 并给出补救命令 (apt install fonts-wqy-zenhei / 放 static/fonts/ / 配 font_path) - .ttc 先提取首个字面再用(仅对 glyf 轮廓的集合有效,如文泉驿); 失败结果缓存 1 分钟,避免公开端点被「每请求重新解析十几 MB」放大 实测:本地实例 apt install fonts-wqy-zenhei 后无需任何配置,启动即打出 font=/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc,/share-card 与单题分享图 从 500 变为 200 image/jpeg,中文与二维码渲染正常。
150 lines
5.4 KiB
Go
150 lines
5.4 KiB
Go
// Copyright 2022 E99p1ant. All rights reserved.
|
||
// Use of this source code is governed by a MIT-style
|
||
// license that can be found in the LICENSE file.
|
||
|
||
package cmd
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"time"
|
||
|
||
"github.com/pkg/errors"
|
||
"github.com/sirupsen/logrus"
|
||
"github.com/uptrace/opentelemetry-go-extra/otellogrus"
|
||
"github.com/uptrace/uptrace-go/uptrace"
|
||
"github.com/urfave/cli/v2"
|
||
|
||
"github.com/tamakyi/TamaBox/internal/conf"
|
||
"github.com/tamakyi/TamaBox/internal/db"
|
||
"github.com/tamakyi/TamaBox/internal/route"
|
||
"github.com/tamakyi/TamaBox/internal/security/honeypot"
|
||
"github.com/tamakyi/TamaBox/internal/shared"
|
||
"github.com/tamakyi/TamaBox/internal/stats"
|
||
"github.com/tamakyi/TamaBox/internal/tracing"
|
||
"github.com/tamakyi/TamaBox/route/question"
|
||
)
|
||
|
||
var Web = &cli.Command{
|
||
Name: "web",
|
||
Usage: "Start web server",
|
||
Action: runWeb,
|
||
}
|
||
|
||
func runWeb(ctx *cli.Context) error {
|
||
if err := conf.Init(); err != nil {
|
||
return errors.Wrap(err, "load configuration")
|
||
}
|
||
|
||
// 解析可信代理网段(用于客户端 IP 解析,避免维护 Cloudflare 庞大 CIDR 清单)。
|
||
if err := shared.InitTrustedProxies(); err != nil {
|
||
return errors.Wrap(err, "init trusted proxies")
|
||
}
|
||
|
||
if conf.App.UptraceDSN != "" {
|
||
uptrace.ConfigureOpentelemetry(
|
||
uptrace.WithDSN(conf.App.UptraceDSN),
|
||
uptrace.WithServiceName("tamabox"),
|
||
uptrace.WithServiceVersion(conf.BuildCommit),
|
||
)
|
||
logrus.WithContext(ctx.Context).Debug("Tracing enabled.")
|
||
}
|
||
|
||
logrus.AddHook(otellogrus.NewHook(otellogrus.WithLevels(
|
||
logrus.PanicLevel,
|
||
logrus.FatalLevel,
|
||
logrus.ErrorLevel,
|
||
logrus.WarnLevel,
|
||
)))
|
||
|
||
dbType := conf.Database.Type
|
||
|
||
var dsn string
|
||
switch dbType {
|
||
case "mysql", "":
|
||
dsn = conf.MySQLDsn()
|
||
case "postgres":
|
||
dsn = conf.PostgresDsn()
|
||
default:
|
||
return errors.Errorf("unknown database type: %q", dbType)
|
||
}
|
||
conf.Database.DSN = dsn
|
||
|
||
_, err := db.Init(dbType, dsn)
|
||
if err != nil {
|
||
return errors.Wrap(err, "connect to database")
|
||
}
|
||
|
||
// 蜜罐命中记录保留期清理:honeypot_hits 是只追加的审计表,写入完全由
|
||
// 外部扫描流量驱动(不可预期、不可节流)。没有自动清理的话,表体积只增不减。
|
||
// 保留期复用 honeypot_retain_days(与 CLI prune 同一个口径),开关为 honeypot_auto_clean。
|
||
//
|
||
// 凭据文件(明文账号密码)也按同一个保留期清理 —— 它们不该比元数据活得更久。
|
||
// 这里独立打开一次目录即可:PurgeBefore 只删「整天都早于保留期」的文件,
|
||
// 与 route 层那个 Append 用的句柄不会争抢同一天的文件。
|
||
stopRetention := make(chan struct{})
|
||
if conf.Security.HoneypotAutoClean {
|
||
var credPurge db.CredPurgeable
|
||
if cs, err := honeypot.NewFileCredStore(conf.Security.HoneypotCredDir); err != nil {
|
||
logrus.WithError(err).Warn("蜜罐凭据目录不可用,凭据文件将不会被自动清理")
|
||
} else {
|
||
credPurge = cs
|
||
}
|
||
db.NewHoneypotRetention(db.Honeypots, credPurge, conf.Security.HoneypotRetainDays, conf.Security.HoneypotMaxRows, 0).Start(stopRetention)
|
||
defer close(stopRetention)
|
||
}
|
||
|
||
logrus.WithContext(ctx.Context).WithField("external_url", conf.App.ExternalURL).Info("Starting web server")
|
||
|
||
// 站点默认语言初始化(default_lang 配置,未配置时回退 zh-CN)
|
||
shared.App.SetLang(conf.App.DefaultLang)
|
||
|
||
// 分享卡字体:缺字体只会在「第一张分享图被请求」时暴露成 500,而 /share-card 是公开端点。
|
||
// 启动时先探一次(顺带把读文件 + 解析的开销挪出请求路径),缺了直接 WARN 并给出补救办法。
|
||
if fontPath, err := question.CheckShareCardFont(); err != nil {
|
||
logrus.WithError(err).Warn("分享卡字体不可用,/share-card 将返回 500:" +
|
||
"装一个 glyf 轮廓的 CJK 字体(apt install fonts-wqy-zenhei)" +
|
||
"或把 ttf/otf/ttc 放进 static/fonts/,也可用 [app] font_path 指定")
|
||
} else {
|
||
logrus.WithField("font", fontPath).Info("分享卡字体已就绪")
|
||
}
|
||
|
||
r := route.New()
|
||
r.Use(tracing.Middleware("TamaBox"))
|
||
|
||
// 全站数据页预热:整页聚合是若干次全表 GROUP BY,若摊在当天第一个访客身上,
|
||
// 首屏要白等一秒以上。这里延后几秒在后台先算一次,把等待挪出请求路径。
|
||
// 失败只告警:页面在真正被访问时会自己重算并(若仍失败)回退到上一次成功的结果。
|
||
go func() {
|
||
time.Sleep(3 * time.Second)
|
||
if _, err := stats.OverviewData(ctx.Context); err != nil {
|
||
logrus.WithError(err).Warn("预热全站统计数据失败")
|
||
}
|
||
}()
|
||
|
||
// 最外层挂载 gzip 压缩,并在 http.Server 上配置超时:
|
||
// - ReadHeaderTimeout 防止慢速客户端占用连接
|
||
// - ReadTimeout / WriteTimeout 防止读写挂死
|
||
// - IdleTimeout 复用 keep-alive 连接
|
||
// listen 留空时回退到 ":port"(监听所有网卡);Cloudflare Tunnel 部署建议设为
|
||
// "127.0.0.1:<port>",使源站端口仅对本地 cloudflared 可见,即使防火墙误配也无法被外部直连。
|
||
addr := conf.Server.Listen
|
||
if addr == "" {
|
||
addr = fmt.Sprintf(":%d", conf.Server.Port)
|
||
}
|
||
server := &http.Server{
|
||
Addr: addr,
|
||
Handler: route.GzipHandler(r),
|
||
ReadHeaderTimeout: 10 * time.Second,
|
||
ReadTimeout: 30 * time.Second,
|
||
WriteTimeout: 60 * time.Second,
|
||
IdleTimeout: 120 * time.Second,
|
||
}
|
||
|
||
logrus.WithContext(ctx.Context).WithField("port", conf.Server.Port).Info("Listening on")
|
||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||
return errors.Wrap(err, "start http server")
|
||
}
|
||
return nil
|
||
}
|