159 lines
4.7 KiB
Go
159 lines
4.7 KiB
Go
package feedback
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"ymhut-box/server/unified-management/internal/config"
|
|
"ymhut-box/server/unified-management/internal/db"
|
|
)
|
|
|
|
func TestSignedMultipartSubmission(t *testing.T) {
|
|
root := t.TempDir()
|
|
cfg := testConfig(root)
|
|
store, err := db.Open(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
service := NewService(cfg, store)
|
|
plain := zipBytes(t, map[string]string{
|
|
"feedback.json": `{"request":{"title":"Crash on launch","type":"issue","severity":"major","contact":"user@example.com","body":"It crashes."}}`,
|
|
"summary.txt": "launch failure",
|
|
})
|
|
encrypted := encryptPackageForTest(t, plain, cfg.PackageEncryptionKey)
|
|
encryptedHash := sha256HexTest(encrypted)
|
|
plainHash := sha256HexTest(plain)
|
|
timestamp := itoa(int(time.Now().Unix()))
|
|
payload := `{"feedbackCode":"FB-20260625-ABCDEF","title":"Crash on launch","type":"issue","severity":"major","contact":"user@example.com","bodyLength":11,"packageEncrypted":true,"encryption":"YMHUTFB1","packageBytes":` + itoa(len(encrypted)) + `,"packageSha256":"` + encryptedHash + `","plainPackageBytes":` + itoa(len(plain)) + `,"plainPackageSha256":"` + plainHash + `","createdAt":"2026-06-25T00:00:00Z"}`
|
|
body := &bytes.Buffer{}
|
|
writer := multipart.NewWriter(body)
|
|
_ = writer.WriteField("payload", payload)
|
|
_ = writer.WriteField("timestamp", timestamp)
|
|
_ = writer.WriteField("nonce", "abc123")
|
|
_ = writer.WriteField("packageSha256", encryptedHash)
|
|
_ = writer.WriteField("signature", SignWithKey(cfg.ClientSignatureKey, timestamp, "abc123", encryptedHash, payload))
|
|
part, err := writer.CreateFormFile("package", "feedback.ymfb")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = io.Copy(part, bytes.NewReader(encrypted))
|
|
_ = writer.Close()
|
|
req := httptest.NewRequest("POST", "/", body)
|
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
|
item, err := service.Submit(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if item.Code != "FB-20260625-ABCDEF" || !strings.Contains(item.IncludedFiles, "feedback.json") {
|
|
t.Fatalf("unexpected item: %#v", item)
|
|
}
|
|
if _, err := store.GetFeedback(item.Code); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestZipPathEscapeRejected(t *testing.T) {
|
|
root := t.TempDir()
|
|
cfg := testConfig(root)
|
|
store, err := db.Open(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer store.Close()
|
|
plain := zipBytes(t, map[string]string{"../escape.txt": "bad", "feedback.json": `{}`})
|
|
if _, err := ReadFeedbackPackageWithGuard(plain, cfg.UploadGuard); err == nil {
|
|
t.Fatal("expected unsafe zip entry error")
|
|
}
|
|
}
|
|
|
|
func testConfig(root string) *config.Config {
|
|
return &config.Config{
|
|
StorageDir: filepath.Join(root, "storage"),
|
|
ClientSignatureKey: "ymhut-box-feedback-client-v1",
|
|
PackageEncryptionKey: "ymhut-box-feedback-package-v1",
|
|
TimestampWindowSeconds: 600,
|
|
MaxRequestBytes: 12 << 20,
|
|
MaxPackageBytes: 10 << 20,
|
|
Database: config.DatabaseConfig{
|
|
Provider: "sqlite",
|
|
SQLitePath: filepath.Join(root, "storage", "unified.sqlite"),
|
|
FailoverEnabled: true,
|
|
HealthIntervalSec: 3600,
|
|
MaxOpenConns: 1,
|
|
MaxIdleConns: 1,
|
|
ConnMaxLifetimeSeconds: 60,
|
|
},
|
|
UploadGuard: config.UploadGuardConfig{MaxZipFiles: 80, MaxDecompressedBytes: 30 << 20, MaxSingleFileBytes: 8 << 20, MaxCompressionRatio: 120, MaxReadableTextBytes: 256 << 10, AllowUnexpectedZipFiles: true},
|
|
}
|
|
}
|
|
|
|
func zipBytes(t *testing.T, files map[string]string) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
writer := zip.NewWriter(&buf)
|
|
for name, body := range files {
|
|
entry, err := writer.Create(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _ = entry.Write([]byte(body))
|
|
}
|
|
if err := writer.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func encryptPackageForTest(t *testing.T, plain []byte, keyMaterial string) []byte {
|
|
t.Helper()
|
|
key := sha256.Sum256([]byte(keyMaterial))
|
|
block, err := aes.NewCipher(key[:])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nonce := []byte("123456789012")
|
|
sealed := gcm.Seal(nil, nonce, plain, []byte(PackageMagic))
|
|
ciphertext := sealed[:len(sealed)-gcm.Overhead()]
|
|
tag := sealed[len(sealed)-gcm.Overhead():]
|
|
out := []byte(PackageMagic)
|
|
out = append(out, nonce...)
|
|
out = append(out, tag...)
|
|
out = append(out, ciphertext...)
|
|
return out
|
|
}
|
|
|
|
func sha256HexTest(data []byte) string {
|
|
sum := sha256.Sum256(data)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func itoa(value int) string {
|
|
if value == 0 {
|
|
return "0"
|
|
}
|
|
var buf [20]byte
|
|
i := len(buf)
|
|
for value > 0 {
|
|
i--
|
|
buf[i] = byte('0' + value%10)
|
|
value /= 10
|
|
}
|
|
return string(buf[i:])
|
|
}
|