后端基础层:模块定义、配置解析、SQLite 存储层、Argon2id 认证与 Markdown 渲染

- internal/config:serve/init/backup/gc 子命令参数解析(--dev 强制 loopback 守卫)
- internal/store:user_version 版本化迁移(仅追加式 + 越界拒启 + --allow-newer)、
  笔记/图片/引用/会话/设置 DAO、并集可见性查询、VACUUM INTO 在线备份、
  回收站 30 天 + 孤儿图 7 天宽限 gc
- internal/auth:Argon2id PHC 串(m=19456,t=2,p=1)、256bit token 与 SHA-256 摘要
- internal/markdown:goldmark(默认转义)+ bluemonday 双保险,摘要纯文本提取
- internal/middleware:安全头(CSP/HSTS/nosniff 等)、错误日志、
  有界令牌桶限流(per-IP 桶上限 + TTL 逐出)、Origin/Referer 同源校验
This commit is contained in:
2026-09-08 08:14:12 +08:00
parent 685f628e26
commit 247e88c4fb
17 changed files with 1786 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
// Package config 解析各子命令的命令行开关。
package config
import (
"flag"
"fmt"
"net"
"strings"
)
// Config 运行时配置,serve 与维护子命令共用。
type Config struct {
Addr string
DataDir string
LogLevel string
LogFormat string // text|json
BehindProxy bool
Dev bool
AllowNewer bool
}
// DBPath SQLite 数据库文件路径。
func (c *Config) DBPath() string { return c.DataDir + "/pure-note.db" }
func addCommonFlags(fs *flag.FlagSet, c *Config) {
fs.StringVar(&c.DataDir, "data-dir", "./data", "数据目录(SQLite 数据库所在)")
fs.BoolVar(&c.AllowNewer, "allow-newer", false, "允许在更新的数据库 schema 版本上运行(跳过版本上界守卫)")
}
func addServeFlags(fs *flag.FlagSet, c *Config) {
addCommonFlags(fs, c)
fs.StringVar(&c.Addr, "addr", ":8080", "HTTP 监听地址")
fs.StringVar(&c.LogLevel, "log-level", "info", "日志级别(debug|info|warn|error)")
fs.StringVar(&c.LogFormat, "log-format", "text", "日志格式(text|json)")
fs.BoolVar(&c.BehindProxy, "behind-proxy", false, "位于可信反向代理之后(取 X-Forwarded-For 最右条目作为客户端 IP)")
fs.BoolVar(&c.Dev, "dev", false, "开发模式:允许非 Secure Cookie,仅允许监听 loopback 地址")
}
// ParseServe 解析 `pure-note serve` 参数。
func ParseServe(args []string) (*Config, error) {
c := &Config{}
fs := flag.NewFlagSet("serve", flag.ContinueOnError)
addServeFlags(fs, c)
if err := fs.Parse(args); err != nil {
return nil, err
}
if c.Dev && !isLoopbackAddr(c.Addr) {
return nil, fmt.Errorf("--dev 仅允许监听 loopback 地址(如 127.0.0.1:8080),当前为 %q", c.Addr)
}
return c, nil
}
// ParseInit 解析 `pure-note init` 参数。
func ParseInit(args []string) (*Config, error) {
c := &Config{}
fs := flag.NewFlagSet("init", flag.ContinueOnError)
addCommonFlags(fs, c)
if err := fs.Parse(args); err != nil {
return nil, err
}
return c, nil
}
// ParseMaint 解析 `pure-note backup` / `pure-note gc` 参数(均只需数据目录)。
// 返回 flag 解析后的剩余位置参数(如 backup 的输出路径)。
func ParseMaint(cmd string, args []string) (*Config, []string, error) {
c := &Config{}
fs := flag.NewFlagSet(cmd, flag.ContinueOnError)
addCommonFlags(fs, c)
if err := fs.Parse(args); err != nil {
return nil, nil, err
}
return c, fs.Args(), nil
}
func isLoopbackAddr(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
// 无端口:整体视为主机名
host = addr
}
if host == "" || strings.EqualFold(host, "localhost") {
return host != ""
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}