mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-07 17:10:29 +00:00
feat(protocol/http, mcpserver, download): add checksum verification, MCP speed tool, pause webhook, and CLI flags (#1542)
This commit is contained in:
1 parent
fa622a4063
commit
6f20790b17
41 files changed
+1852
-25
No files matched your search
@@ -1,11 +1,83 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReqExtra struct {
|
||||
Method string `json:"method"`
|
||||
Header map[string]string `json:"header"`
|
||||
Body string `json:"body"`
|
||||
}
|
||||
|
||||
// ChecksumOption specifies an expected checksum to verify against
|
||||
// the completed download. Only supported for HTTP downloads.
|
||||
type ChecksumOption struct {
|
||||
Algorithm string `json:"algorithm"` // "md5" | "sha1" | "sha256"
|
||||
Expected string `json:"expected"` // hex-encoded expected hash
|
||||
}
|
||||
|
||||
// IsEmpty returns true when chk is nil, or when both Algorithm and Expected
|
||||
// are blank after trimming whitespace.
|
||||
func (chk *ChecksumOption) IsEmpty() bool {
|
||||
if chk == nil {
|
||||
return true
|
||||
}
|
||||
return strings.TrimSpace(chk.Algorithm) == "" && strings.TrimSpace(chk.Expected) == ""
|
||||
}
|
||||
|
||||
// NormalizedAlgorithm returns the canonical lower-cased algorithm ("md5", "sha1", "sha256")
|
||||
// and true if recognized, or ("", false) if unsupported.
|
||||
func (chk *ChecksumOption) NormalizedAlgorithm() (string, bool) {
|
||||
if chk == nil {
|
||||
return "", false
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(chk.Algorithm)) {
|
||||
case "md5":
|
||||
return "md5", true
|
||||
case "sha1", "sha-1":
|
||||
return "sha1", true
|
||||
case "sha256", "sha-256":
|
||||
return "sha256", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks whether the checksum option is valid.
|
||||
// Empty options return nil (no verification).
|
||||
func (chk *ChecksumOption) Validate() error {
|
||||
if chk.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
algo, ok := chk.NormalizedAlgorithm()
|
||||
if !ok {
|
||||
return fmt.Errorf("unsupported checksum algorithm: %s", chk.Algorithm)
|
||||
}
|
||||
expected := strings.TrimSpace(chk.Expected)
|
||||
if expected == "" {
|
||||
return fmt.Errorf("checksum expected hash is required when algorithm is set")
|
||||
}
|
||||
var expectedLen int
|
||||
switch algo {
|
||||
case "md5":
|
||||
expectedLen = 32
|
||||
case "sha1":
|
||||
expectedLen = 40
|
||||
case "sha256":
|
||||
expectedLen = 64
|
||||
}
|
||||
if len(expected) != expectedLen {
|
||||
return fmt.Errorf("invalid checksum hash length for %s: expected %d characters, got %d", algo, expectedLen, len(expected))
|
||||
}
|
||||
if _, err := hex.DecodeString(expected); err != nil {
|
||||
return fmt.Errorf("invalid checksum hash for %s: expected valid hexadecimal string", algo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type OptsExtra struct {
|
||||
Connections int `json:"connections"`
|
||||
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
|
||||
@@ -21,6 +93,8 @@ type OptsExtra struct {
|
||||
ArchivePassword string `json:"archivePassword"`
|
||||
// DeleteAfterExtract when true, deletes the archive file after successful extraction
|
||||
DeleteAfterExtract bool `json:"deleteAfterExtract"`
|
||||
// Checksum verifies the completed file against an expected hash after download
|
||||
Checksum *ChecksumOption `json:"checksum,omitempty"`
|
||||
}
|
||||
|
||||
// Stats for download
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOptsExtra_Checksum(t *testing.T) {
|
||||
extra := &OptsExtra{
|
||||
Checksum: &ChecksumOption{
|
||||
Algorithm: "sha256",
|
||||
Expected: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
},
|
||||
}
|
||||
if extra.Checksum.Algorithm != "sha256" || extra.Checksum.Expected != "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" {
|
||||
t.Fatalf("unexpected checksum: %+v", extra.Checksum)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChecksumOption_Validate(t *testing.T) {
|
||||
validMd5 := "0123456789abcdef0123456789abcdef"
|
||||
validSha1 := "0123456789abcdef0123456789abcdef01234567"
|
||||
validSha256 := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
option *ChecksumOption
|
||||
wantErr bool
|
||||
errSubstr string
|
||||
}{
|
||||
{
|
||||
name: "nil option",
|
||||
option: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty option (empty strings)",
|
||||
option: &ChecksumOption{Algorithm: "", Expected: ""},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty option (whitespace strings)",
|
||||
option: &ChecksumOption{Algorithm: " ", Expected: " "},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid md5 lowercase",
|
||||
option: &ChecksumOption{Algorithm: "md5", Expected: validMd5},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid md5 uppercase",
|
||||
option: &ChecksumOption{Algorithm: "MD5", Expected: strings.ToUpper(validMd5)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha1 lowercase",
|
||||
option: &ChecksumOption{Algorithm: "sha1", Expected: validSha1},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha-1 alias",
|
||||
option: &ChecksumOption{Algorithm: "SHA-1", Expected: strings.ToUpper(validSha1)},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha256 lowercase",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: validSha256},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha-256 alias",
|
||||
option: &ChecksumOption{Algorithm: "sha-256", Expected: validSha256},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid sha256 with whitespace around hash and algorithm",
|
||||
option: &ChecksumOption{Algorithm: " sha256 ", Expected: " " + validSha256 + " "},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "unknown algorithm",
|
||||
option: &ChecksumOption{Algorithm: "crc32", Expected: "12345678"},
|
||||
wantErr: true,
|
||||
errSubstr: "unsupported checksum algorithm",
|
||||
},
|
||||
{
|
||||
name: "algorithm set but hash blank",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: " "},
|
||||
wantErr: true,
|
||||
errSubstr: "checksum expected hash is required",
|
||||
},
|
||||
{
|
||||
name: "sha256 with abc (short length)",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: "abc"},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 64 characters, got 3",
|
||||
},
|
||||
{
|
||||
name: "sha256 with 64 x g (non-hex)",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: strings.Repeat("g", 64)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected valid hexadecimal string",
|
||||
},
|
||||
{
|
||||
name: "sha256 with 63 x a (off-by-one short)",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: strings.Repeat("a", 63)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 64 characters, got 63",
|
||||
},
|
||||
{
|
||||
name: "sha256 with 65 x a (off-by-one long)",
|
||||
option: &ChecksumOption{Algorithm: "sha256", Expected: strings.Repeat("a", 65)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 64 characters, got 65",
|
||||
},
|
||||
{
|
||||
name: "md5 with 31 chars",
|
||||
option: &ChecksumOption{Algorithm: "md5", Expected: strings.Repeat("a", 31)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 32 characters, got 31",
|
||||
},
|
||||
{
|
||||
name: "md5 with 33 chars",
|
||||
option: &ChecksumOption{Algorithm: "md5", Expected: strings.Repeat("a", 33)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 32 characters, got 33",
|
||||
},
|
||||
{
|
||||
name: "md5 with sha256-length hash (64 chars)",
|
||||
option: &ChecksumOption{Algorithm: "md5", Expected: validSha256},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 32 characters, got 64",
|
||||
},
|
||||
{
|
||||
name: "sha1 with 39 chars",
|
||||
option: &ChecksumOption{Algorithm: "sha1", Expected: strings.Repeat("a", 39)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 40 characters, got 39",
|
||||
},
|
||||
{
|
||||
name: "sha1 with 41 chars",
|
||||
option: &ChecksumOption{Algorithm: "sha1", Expected: strings.Repeat("a", 41)},
|
||||
wantErr: true,
|
||||
errSubstr: "expected 40 characters, got 41",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.option.Validate()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr && tt.errSubstr != "" {
|
||||
if err == nil || !strings.Contains(err.Error(), tt.errSubstr) {
|
||||
t.Fatalf("Validate() error = %v, want substring %q", err, tt.errSubstr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user