Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
package support
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/google/uuid"
)
const (
maxKBImageBytes = 2 << 20 // 2 MiB
kbMediaSubdir = "support-kb"
// KBImageAdminURLPrefix is the platform-admin serve path returned after upload.
KBImageAdminURLPrefix = "/api/admin/support/kb/images/"
// KBImagePublicPathPrefix is the permanent public serve path (HMAC-signed).
KBImagePublicPathPrefix = "/api/public/support-kb/"
)
var (
ErrKBImageInvalidType = errors.New("image must be PNG, JPEG, or WebP")
ErrKBImageTooLarge = errors.New("image exceeds 2 MiB limit")
ErrKBImageInvalidName = errors.New("invalid image filename")
ErrKBImageNotFound = errors.New("image not found")
ErrKBImageForbidden = errors.New("image access forbidden")
ErrKBImageBadSig = errors.New("invalid image signature")
ErrKBUploadDirMissing = errors.New("upload directory not configured")
kbImageNameRE = regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(png|jpe?g|webp)$`)
)
// KBImageUpload is the admin upload response (insert markdown_url into body_md).
type KBImageUpload struct {
Filename string `json:"filename"`
ContentType string `json:"content_type"`
Size int64 `json:"size"`
AdminURL string `json:"admin_url"`
MarkdownURL string `json:"markdown_url"`
Markdown string `json:"markdown"`
}
type kbImageKind struct {
ext string
contentType string
}
// SaveKBImage validates mime/size and stores under UPLOAD_DIR/support-kb (not web-executable).
func SaveKBImage(uploadDir, publicAPIURL, signingSecret, originalName, declaredType string, r io.Reader) (KBImageUpload, error) {
uploadDir = strings.TrimSpace(uploadDir)
if uploadDir == "" {
return KBImageUpload{}, ErrKBUploadDirMissing
}
limited := io.LimitReader(r, maxKBImageBytes+1)
data, err := io.ReadAll(limited)
if err != nil {
return KBImageUpload{}, err
}
if int64(len(data)) > maxKBImageBytes {
return KBImageUpload{}, ErrKBImageTooLarge
}
kind, err := detectKBImage(data, originalName, declaredType)
if err != nil {
return KBImageUpload{}, err
}
fileID := uuid.New()
name := strings.ToLower(fileID.String() + "." + kind.ext)
dir := filepath.Join(uploadDir, kbMediaSubdir)
if err := os.MkdirAll(dir, 0o750); err != nil {
return KBImageUpload{}, err
}
abs := filepath.Join(dir, name)
if err := os.WriteFile(abs, data, 0o640); err != nil {
return KBImageUpload{}, err
}
mdURL, err := PublicKBImageURL(publicAPIURL, signingSecret, name)
if err != nil {
_ = os.Remove(abs)
return KBImageUpload{}, err
}
adminURL := KBImageAdminURLPrefix + name
return KBImageUpload{
Filename: name,
ContentType: kind.contentType,
Size: int64(len(data)),
AdminURL: adminURL,
MarkdownURL: mdURL,
Markdown: fmt.Sprintf("![image](%s)", mdURL),
}, nil
}
// ResolveKBImagePath returns the absolute filesystem path for a KB image.
func ResolveKBImagePath(uploadDir, name string) (string, error) {
name, err := sanitizeKBImageName(name)
if err != nil {
return "", err
}
uploadDir = strings.TrimSpace(uploadDir)
if uploadDir == "" {
return "", ErrKBUploadDirMissing
}
base := filepath.Join(uploadDir, kbMediaSubdir)
abs := filepath.Join(base, name)
rel, err := filepath.Rel(base, abs)
if err != nil || strings.HasPrefix(rel, "..") {
return "", ErrKBImageForbidden
}
return abs, nil
}
// OpenKBImage opens a stored KB image for reading.
func OpenKBImage(uploadDir, name string) (*os.File, string, error) {
abs, err := ResolveKBImagePath(uploadDir, name)
if err != nil {
return nil, "", err
}
f, err := os.Open(abs)
if err != nil {
if os.IsNotExist(err) {
return nil, "", ErrKBImageNotFound
}
return nil, "", err
}
return f, contentTypeForKBImageName(name), nil
}
// PublicKBImageURL builds a permanent absolute URL with HMAC signature (no expiry).
func PublicKBImageURL(publicAPIURL, secret, filename string) (string, error) {
filename, err := sanitizeKBImageName(filename)
if err != nil {
return "", err
}
secret = strings.TrimSpace(secret)
if secret == "" {
return "", errors.New("token signing secret not configured")
}
sig := signKBImage(secret, filename)
base := strings.TrimRight(strings.TrimSpace(publicAPIURL), "/")
if base == "" {
base = "http://localhost:28471"
}
q := url.Values{}
q.Set("sig", sig)
return fmt.Sprintf("%s%s%s?%s", base, KBImagePublicPathPrefix, filename, q.Encode()), nil
}
// VerifyKBImageSig checks the permanent HMAC for a public KB image request.
func VerifyKBImageSig(secret, filename, sig string) error {
filename, err := sanitizeKBImageName(filename)
if err != nil {
return err
}
if strings.TrimSpace(secret) == "" || strings.TrimSpace(sig) == "" {
return ErrKBImageBadSig
}
expected := signKBImage(secret, filename)
if !hmac.Equal([]byte(expected), []byte(strings.TrimSpace(sig))) {
return ErrKBImageBadSig
}
return nil
}
func signKBImage(secret, filename string) string {
mac := hmac.New(sha256.New, []byte(secret))
_, _ = mac.Write([]byte("kb|" + filename))
return hex.EncodeToString(mac.Sum(nil))
}
func sanitizeKBImageName(name string) (string, error) {
name = filepath.Base(strings.TrimSpace(name))
if name == "" || name == "." || name == ".." {
return "", ErrKBImageInvalidName
}
if strings.Contains(name, "..") || strings.ContainsAny(name, `/\`) {
return "", ErrKBImageInvalidName
}
if !kbImageNameRE.MatchString(name) {
return "", ErrKBImageInvalidName
}
return strings.ToLower(name), nil
}
func detectKBImage(data []byte, originalName, declaredType string) (kbImageKind, error) {
if len(data) < 12 {
return kbImageKind{}, ErrKBImageInvalidType
}
extFromName := strings.ToLower(filepath.Ext(originalName))
declared := strings.ToLower(strings.TrimSpace(declaredType))
switch {
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}):
if declared != "" && !strings.Contains(declared, "png") && declared != "application/octet-stream" {
return kbImageKind{}, ErrKBImageInvalidType
}
if extFromName != "" && extFromName != ".png" {
return kbImageKind{}, ErrKBImageInvalidType
}
return kbImageKind{ext: "png", contentType: "image/png"}, nil
case bytes.HasPrefix(data, []byte{0xff, 0xd8, 0xff}):
if declared != "" && !strings.Contains(declared, "jpeg") && !strings.Contains(declared, "jpg") && declared != "application/octet-stream" {
return kbImageKind{}, ErrKBImageInvalidType
}
if extFromName != "" && extFromName != ".jpg" && extFromName != ".jpeg" {
return kbImageKind{}, ErrKBImageInvalidType
}
return kbImageKind{ext: "jpg", contentType: "image/jpeg"}, nil
case isKBWebP(data):
if declared != "" && !strings.Contains(declared, "webp") && declared != "application/octet-stream" {
return kbImageKind{}, ErrKBImageInvalidType
}
if extFromName != "" && extFromName != ".webp" {
return kbImageKind{}, ErrKBImageInvalidType
}
return kbImageKind{ext: "webp", contentType: "image/webp"}, nil
default:
_ = http.DetectContentType(data)
return kbImageKind{}, ErrKBImageInvalidType
}
}
func isKBWebP(data []byte) bool {
return len(data) >= 12 &&
bytes.Equal(data[0:4], []byte("RIFF")) &&
bytes.Equal(data[8:12], []byte("WEBP"))
}
func contentTypeForKBImageName(name string) string {
switch strings.ToLower(filepath.Ext(name)) {
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".webp":
return "image/webp"
default:
return "application/octet-stream"
}
}
// MediaClientError maps KB image errors for HTTP responses.
func MediaClientError(err error) (msg string, ok bool) {
switch {
case err == nil:
return "", false
case errors.Is(err, ErrKBImageInvalidType),
errors.Is(err, ErrKBImageTooLarge),
errors.Is(err, ErrKBImageInvalidName),
errors.Is(err, ErrKBImageBadSig),
errors.Is(err, ErrKBUploadDirMissing):
return err.Error(), true
default:
return "", false
}
}