@@ -0,0 +1,158 @@
|
||||
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:])
|
||||
}
|
||||
Reference in New Issue
Block a user