docs(gateway): gateway 剩余文件注释改中文(逐行 1:1,代码零字节改动)
主人 2026-10-03 拍板「注释一律中文」;gateway 至此整包注释中文化完成(本会话共 3 批: 已推库 16 个文件 + 本批剩余文件)。 校验:机械校验(非注释改动行)= 空;剥注释后代码文本逐行比对全等(不一致文件数 0); 全树 go test ./... 58 包 ok / 0 FAIL;交叉编译绿。
This commit is contained in:
1 parent
91e8d7a029
commit
074ffc4ebf
73 files changed
+2529
-2529
No files matched your search
@@ -17,9 +17,9 @@ import (
|
||||
apperr "git.yeij.top/AskaEth/Cyrene_v2/internal/errors"
|
||||
)
|
||||
|
||||
// maxUploadBytes caps a single uploaded image at 5 MiB. The multipart envelope
|
||||
// (boundaries, part headers) gets a little headroom on top of that; the decoded
|
||||
// file itself is checked against this exact limit.
|
||||
// maxUploadBytes 把单张上传图片限制在 5 MiB。multipart 信封
|
||||
// (边界、部件头)在其上多留一点余量;解码后的
|
||||
// 文件本身按这个确切上限校验。
|
||||
const (
|
||||
maxUploadBytes = 5 << 20
|
||||
uploadMultipartSlack = 1 << 20
|
||||
@@ -27,16 +27,16 @@ const (
|
||||
uploadSniffBytes = 512
|
||||
)
|
||||
|
||||
// uploadFilePrefix is the same-origin static route that serves stored uploads
|
||||
// (registered in Handler). It lives under /api/v1/ so the gateway mux — not the
|
||||
// SPA fallback — owns it, and so it is part of the versioned client contract.
|
||||
// uploadFilePrefix 是提供已存上传的同源静态路由
|
||||
// (在 Handler 中注册)。它位于 /api/v1/ 之下,因此归网关 mux 而非
|
||||
// SPA 回退所有,也因此属于版本化客户端契约。
|
||||
const uploadFilePrefix = "/api/v1/files/"
|
||||
|
||||
// uploadResponse is the POST /api/v1/upload success payload. URL is an absolute
|
||||
// http(s) URL built from the request authority so it can be handed straight to
|
||||
// chat.image: the vision layer classifies only http(s):// as a URL and would
|
||||
// otherwise treat a bare path as base64. Name is the server-generated storage
|
||||
// name; the client's original filename is never echoed back or persisted.
|
||||
// uploadResponse 是 POST /api/v1/upload 的成功载荷。URL 是由请求
|
||||
// 权威构建的绝对 http(s) URL,因此能直接交给
|
||||
// chat.image:视觉层只把 http(s):// 归类为 URL,否则会把
|
||||
// 裸路径当作 base64。Name 是服务端生成的存储
|
||||
// 名;客户端原始文件名绝不回显或持久化。
|
||||
type uploadResponse struct {
|
||||
URL string `json:"url"`
|
||||
Size int64 `json:"size"`
|
||||
@@ -44,12 +44,12 @@ type uploadResponse struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// handleUpload accepts a multipart/form-data request with a single `file` part,
|
||||
// validates it is a raster image within the size limit, stores it under
|
||||
// <dataDir>/uploads under a cryptographically random name, audits the write and
|
||||
// returns the URL to attach as chat.image. It is owner/admin only (registered in
|
||||
// Handler). The client's original filename is deliberately ignored: it is
|
||||
// attacker-controlled and could carry a path separator or overwrite a file.
|
||||
// handleUpload 接受带单个 `file` 部件的 multipart/form-data 请求,
|
||||
// 校验它是在大小限制内的光栅图片,并以密码学随机的
|
||||
// 名字存到 <dataDir>/uploads 下,审计该写操作,
|
||||
// 返回可作为 chat.image 附上的 URL。它仅 owner/admin(注册在
|
||||
// Handler 中)。客户端原始文件名被刻意忽略:它由
|
||||
// 攻击者控制,可能携带路径分隔符或覆盖文件。
|
||||
func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadBytes+uploadMultipartSlack)
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
@@ -104,12 +104,12 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// guardUploadPaths rejects a static-upload request whose decoded path contains a
|
||||
// parent-directory segment, before ServeMux can clean it and redirect the client
|
||||
// to a non-upload path (which the SPA would then answer with index.html). The
|
||||
// {name} route only ever matches one segment, so a `..` can only be an escape
|
||||
// attempt; answering 404 here keeps every traversal shape uniformly rejected.
|
||||
// Non-upload paths pass through untouched.
|
||||
// guardUploadPaths 拒绝解码后路径含父目录段的静态上传
|
||||
// 请求,赶在 ServeMux 清理它并把客户端重定向到一个非上传
|
||||
// 路径(SPA 随后会用 index.html 应答)之前。{name} 路由
|
||||
// 只匹配一个段,因此 `..` 只可能是逃逸尝试;这里返回 404
|
||||
// 使每一种穿越形态都被统一拒绝。非上传路径
|
||||
// 原样放行。
|
||||
func guardUploadPaths(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, uploadFilePrefix) && hasDotDotSegment(r.URL.Path) {
|
||||
@@ -120,7 +120,7 @@ func guardUploadPaths(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// hasDotDotSegment reports whether p has a literal ".." path segment.
|
||||
// hasDotDotSegment 报告 p 是否有字面的 ".." 路径段。
|
||||
func hasDotDotSegment(p string) bool {
|
||||
for _, seg := range strings.Split(p, "/") {
|
||||
if seg == ".." {
|
||||
@@ -130,11 +130,11 @@ func hasDotDotSegment(p string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// handleUploadFile serves a stored upload from <dataDir>/uploads. It is
|
||||
// unauthenticated by design: the returned URL is handed to chat.image and to
|
||||
// <img> tags, neither of which can attach a bearer token. The random 128-bit
|
||||
// name is the capability; the handler accepts only a strict hex+known-extension
|
||||
// name and never lets a request path escape the uploads directory.
|
||||
// handleUploadFile 从 <dataDir>/uploads 提供一份已存上传。它
|
||||
// 按设计不认证:返回的 URL 会交给 chat.image 与
|
||||
// <img> 标签,二者都无法附带 bearer token。随机的 128 位
|
||||
// 名字就是能力;handler 只接受严格的十六进制+已知扩展名
|
||||
// 名字,且绝不让请求路径逃出 uploads 目录。
|
||||
func (s *Server) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.PathValue("name")
|
||||
if !validUploadName(name) {
|
||||
@@ -173,8 +173,8 @@ func (s *Server) handleUploadFile(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeContent(w, r, name, info.ModTime(), f)
|
||||
}
|
||||
|
||||
// writeUploadBodyError maps a multipart parse failure to its wire error: an
|
||||
// over-limit body is 413, anything else (not multipart, malformed part) is 400.
|
||||
// writeUploadBodyError 把 multipart 解析失败映射为其线上错误:
|
||||
// 超限请求体是 413,其他(非 multipart、部件畸形)是 400。
|
||||
func writeUploadBodyError(w http.ResponseWriter, err error) {
|
||||
var maxErr *http.MaxBytesError
|
||||
if errors.As(err, &maxErr) {
|
||||
@@ -184,9 +184,9 @@ func writeUploadBodyError(w http.ResponseWriter, err error) {
|
||||
writeError(w, apperr.Wrap(apperr.ErrBadRequest, "bad_request", "invalid multipart body"))
|
||||
}
|
||||
|
||||
// sniffImageMIME detects the content type of the upload head with the stdlib
|
||||
// sniffer (never the client-supplied Content-Type header, which is untrusted)
|
||||
// and rewinds the part so the caller can copy it in full.
|
||||
// sniffImageMIME 用标准库嗅探器检测上传头部的类型
|
||||
// (绝不用客户端提供的 Content-Type 头,它不可信),
|
||||
// 并回绕部件,使调用方能完整复制它。
|
||||
func sniffImageMIME(f multipart.File) (string, error) {
|
||||
buf := make([]byte, uploadSniffBytes)
|
||||
n, err := io.ReadFull(f, buf)
|
||||
@@ -199,9 +199,9 @@ func sniffImageMIME(f multipart.File) (string, error) {
|
||||
return http.DetectContentType(buf[:n]), nil
|
||||
}
|
||||
|
||||
// uploadExtForMIME maps a sniffed image MIME to the extension used on disk. The
|
||||
// whitelist is deliberately raster-only: SVG (and HTML) would be served inline
|
||||
// and could execute script, so they are not accepted.
|
||||
// uploadExtForMIME 把嗅探出的图片 MIME 映射为磁盘上使用的扩展名。
|
||||
// 白名单刻意只含光栅:SVG(与 HTML)会被内联服务
|
||||
// 并可能执行脚本,因此不被接受。
|
||||
func uploadExtForMIME(mime string) (string, bool) {
|
||||
switch mime {
|
||||
case "image/png":
|
||||
@@ -219,9 +219,9 @@ func uploadExtForMIME(mime string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// uploadMIMEForExt returns the Content-Type for a stored upload extension. It is
|
||||
// the read-side twin of uploadExtForMIME: only whitelisted extensions resolve,
|
||||
// which is what makes validUploadName strict.
|
||||
// uploadMIMEForExt 返回已存上传扩展名对应的 Content-Type。它是
|
||||
// uploadExtForMIME 的读侧孪生:只有白名单内的扩展名能解析,
|
||||
// 这正是 validUploadName 严格的原因。
|
||||
func uploadMIMEForExt(ext string) (string, bool) {
|
||||
switch strings.ToLower(ext) {
|
||||
case ".png":
|
||||
@@ -239,10 +239,10 @@ func uploadMIMEForExt(ext string) (string, bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// storeUpload writes f to <dataDir>/uploads/<random>.<ext> and returns the
|
||||
// generated name. It writes to a dot-prefixed temp file first and renames it
|
||||
// into place, so a partial upload is never visible under a servable name. The
|
||||
// directory is created 0700 and the file inherits the temp file's 0600 mode.
|
||||
// storeUpload 把 f 写入 <dataDir>/uploads/<random>.<ext> 并返回
|
||||
// 生成的名字。它先写一个点前缀的临时文件再把它重命名
|
||||
// 到位,因此部分上传绝不会以可服务的名字可见。
|
||||
// 目录以 0700 创建,文件继承临时文件的 0600 权限。
|
||||
func (s *Server) storeUpload(f io.Reader, ext string) (string, error) {
|
||||
dir, err := s.uploadDir()
|
||||
if err != nil {
|
||||
@@ -276,10 +276,10 @@ func (s *Server) storeUpload(f io.Reader, ext string) (string, error) {
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// uploadDir returns <dataDir>/uploads. The resolved data dir is injected by the
|
||||
// composition root (Deps.DataDir, the same value resolveDataDir hands the DB and
|
||||
// the runtime configs); the configured cfg.DataDir is the fallback so a Server
|
||||
// built without the injection still resolves its uploads.
|
||||
// uploadDir 返回 <dataDir>/uploads。解析后的数据目录由
|
||||
// 组合根注入(Deps.DataDir,即 resolveDataDir 交给 DB 与
|
||||
// 运行时配置的同一个值);已配置的 cfg.DataDir 是回退,使
|
||||
// 未注入构建的 Server 仍能解析其上传目录。
|
||||
func (s *Server) uploadDir() (string, error) {
|
||||
base := strings.TrimSpace(s.dataDir)
|
||||
if base == "" {
|
||||
@@ -291,8 +291,8 @@ func (s *Server) uploadDir() (string, error) {
|
||||
return filepath.Join(base, "uploads"), nil
|
||||
}
|
||||
|
||||
// randomUploadName returns a 128-bit random hex name with ext appended. It is
|
||||
// the only filename component ever used: the client's original name is dropped.
|
||||
// randomUploadName 返回追加 ext 的 128 位随机十六进制名。它
|
||||
// 是唯一被使用的文件名组成部分:客户端原始名被丢弃。
|
||||
func randomUploadName(ext string) (string, error) {
|
||||
buf := make([]byte, uploadNameBytes)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
@@ -301,10 +301,10 @@ func randomUploadName(ext string) (string, error) {
|
||||
return hex.EncodeToString(buf) + ext, nil
|
||||
}
|
||||
|
||||
// validUploadName reports whether name is exactly a 32-lowercase-hex stem plus a
|
||||
// whitelisted image extension. This is the path-traversal guard for the static
|
||||
// route: separators, dots, dots-dots and any other shape are rejected before the
|
||||
// filesystem is touched.
|
||||
// validUploadName 报告 name 是否恰好是 32 位小写十六进制主名
|
||||
// 加上白名单内的图片扩展名。这是静态路由的路径穿越
|
||||
// 防护:分隔符、点、双点及任何其他形态都在
|
||||
// 触碰文件系统之前被拒绝。
|
||||
func validUploadName(name string) bool {
|
||||
if name == "" || name != filepath.Base(name) || strings.ContainsAny(name, "/\\") {
|
||||
return false
|
||||
@@ -322,15 +322,15 @@ func validUploadName(name string) bool {
|
||||
return ok
|
||||
}
|
||||
|
||||
// isLowerHex reports whether c is an ASCII lowercase hex digit.
|
||||
// isLowerHex 报告 c 是否是 ASCII 小写十六进制数字。
|
||||
func isLowerHex(c rune) bool {
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')
|
||||
}
|
||||
|
||||
// uploadURL builds the absolute, same-origin URL for a stored upload. The scheme
|
||||
// honours X-Forwarded-Proto (reverse proxy) and the authority is the request
|
||||
// host, sanitized so a malformed Host header cannot smuggle path syntax into the
|
||||
// returned URL; the configured listen address is the fallback.
|
||||
// uploadURL 为一份已存上传构建绝对同源 URL。scheme
|
||||
// 遵循 X-Forwarded-Proto(反向代理),权威是请求
|
||||
// host,并做净化,使畸形的 Host 头无法把路径语法夹带进
|
||||
// 返回的 URL;已配置的监听地址是回退。
|
||||
func (s *Server) uploadURL(r *http.Request, name string) string {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
@@ -346,8 +346,8 @@ func (s *Server) uploadURL(r *http.Request, name string) string {
|
||||
return scheme + "://" + host + uploadFilePrefix + name
|
||||
}
|
||||
|
||||
// forwardedProto returns the first X-Forwarded-Proto value when it is http or
|
||||
// https, else "".
|
||||
// forwardedProto 在首个 X-Forwarded-Proto 值为 http 或
|
||||
// https 时返回它,否则返回 ""。
|
||||
func forwardedProto(r *http.Request) string {
|
||||
v := r.Header.Get("X-Forwarded-Proto")
|
||||
if v == "" {
|
||||
@@ -360,8 +360,8 @@ func forwardedProto(r *http.Request) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// sanitizeHost rejects a host value containing path, userinfo or whitespace
|
||||
// syntax; an empty result makes uploadURL fall back to the configured address.
|
||||
// sanitizeHost 拒绝含路径、userinfo 或空白语法的 host
|
||||
// 值;结果为空会让 uploadURL 回退到已配置的地址。
|
||||
func sanitizeHost(host string) string {
|
||||
host = strings.TrimSpace(host)
|
||||
if host == "" || strings.ContainsAny(host, "/\\@ \t") {
|
||||
|
||||
Reference in new issue
Block a user