Skip to content

Commit dd6251f

Browse files
M09Icclaude
andcommitted
fix: prevent send-on-closed-channel panic and baseline invalid on raw parse failure
- Replace close(additionCh) with additionClosed atomic.Bool flag to prevent panic when async producers (redirect/crawl/retry/append) write after pool shutdown - Guard addAddition() with ctx.Err() and additionClosed check before send - ParseRawResponse failure no longer marks baseline as invalid or returns early; log at debug level instead - Read Location header from live response (resp.GetHeader) instead of re-parsed bl.Response.Header for reliability - Add ParseRawResponse boundary tests (nil, empty, truncated, binary, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1690565 commit dd6251f

4 files changed

Lines changed: 146 additions & 15 deletions

File tree

core/baseline/baseline.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package baseline
33
import (
44
"bytes"
55
"github.com/chainreactors/fingers/common"
6+
"github.com/chainreactors/logs"
67
"github.com/chainreactors/parsers"
78
"github.com/chainreactors/spray/core/ihttp"
89
"github.com/chainreactors/spray/pkg"
@@ -53,15 +54,14 @@ func NewBaseline(u, host string, resp *ihttp.Response) *Baseline {
5354
bl.Raw = append(bl.Header, bl.Body...)
5455
bl.Response, err = pkg.ParseRawResponse(bl.Raw)
5556
if err != nil {
56-
bl.IsValid = false
57-
bl.Reason = pkg.ErrResponseError.Error()
58-
bl.ErrString = err.Error()
59-
return bl
57+
// raw 重解析失败不影响 baseline 有效性,live response 已提供所有需要的数据
58+
logs.Log.Debugf("ParseRawResponse failed for %s: %s", u, err.Error())
6059
}
61-
if r := bl.Response.Header.Get("Location"); r != "" {
60+
// 始终从 live response 读取 Location
61+
if r := resp.GetHeader("Location"); r != "" {
6262
bl.RedirectURL = r
6363
} else {
64-
bl.RedirectURL = bl.Response.Header.Get("location")
64+
bl.RedirectURL = resp.GetHeader("location")
6565
}
6666

6767
bl.Dir = bl.IsDir()

core/pool/brutepool.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -678,8 +678,9 @@ func (pool *BrutePool) Close() {
678678
// 等待缓存的待处理任务完成
679679
time.Sleep(time.Duration(100) * time.Millisecond)
680680
}
681-
close(pool.additionCh) // 关闭addition管道
682-
//close(pool.checkCh) // 关闭check管道
681+
pool.additionClosed.Store(true)
682+
// additionCh 可能仍有异步 producer(redirect/crawl/retry/append),
683+
// 依赖 closeCh/ctx 停止消费循环,不直接关闭 channel
683684
pool.Statistor.EndTime = time.Now().Unix()
684685
pool.reqPool.Release()
685686
pool.scopePool.Release()

core/pool/pool.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ type BasePool struct {
2121
ctx context.Context
2222
processCh chan *baseline.Baseline // 待处理的baseline
2323

24-
reqCount int
25-
failedCount int
26-
additionCh chan *Unit
27-
closeCh chan struct{}
28-
wg *sync.WaitGroup
29-
isFallback atomic.Bool
24+
reqCount int
25+
failedCount int
26+
additionCh chan *Unit
27+
additionClosed atomic.Bool
28+
closeCh chan struct{}
29+
wg *sync.WaitGroup
30+
isFallback atomic.Bool
3031
}
3132

3233
func (pool *BasePool) doRetry(bl *baseline.Baseline) {
@@ -44,11 +45,18 @@ func (pool *BasePool) doRetry(bl *baseline.Baseline) {
4445
}
4546

4647
func (pool *BasePool) addAddition(u *Unit) {
48+
if pool.ctx.Err() != nil || pool.additionClosed.Load() {
49+
return
50+
}
51+
4752
pool.wg.Add(1)
4853
select {
54+
case <-pool.ctx.Done():
55+
pool.wg.Done()
56+
return
4957
case pool.additionCh <- u:
58+
return
5059
default:
51-
// 强行屏蔽报错, 防止goroutine泄露
5260
go func() {
5361
select {
5462
case pool.additionCh <- u:

pkg/utils_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package pkg
2+
3+
import (
4+
"strings"
5+
"testing"
6+
)
7+
8+
func TestParseRawResponse(t *testing.T) {
9+
t.Run("valid complete response", func(t *testing.T) {
10+
raw := "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 5\r\n\r\nhello"
11+
resp, err := ParseRawResponse([]byte(raw))
12+
if err != nil {
13+
t.Fatalf("unexpected error: %v", err)
14+
}
15+
if resp.StatusCode != 200 {
16+
t.Fatalf("expected status 200, got %d", resp.StatusCode)
17+
}
18+
if ct := resp.Header.Get("Content-Type"); ct != "text/html" {
19+
t.Fatalf("expected Content-Type text/html, got %s", ct)
20+
}
21+
})
22+
23+
t.Run("nil input", func(t *testing.T) {
24+
_, err := ParseRawResponse(nil)
25+
if err == nil {
26+
t.Fatal("expected error for nil input")
27+
}
28+
})
29+
30+
t.Run("empty input", func(t *testing.T) {
31+
_, err := ParseRawResponse([]byte{})
32+
if err == nil {
33+
t.Fatal("expected error for empty input")
34+
}
35+
})
36+
37+
t.Run("status line only no headers", func(t *testing.T) {
38+
raw := "HTTP/1.1 200 OK\r\n\r\n"
39+
resp, err := ParseRawResponse([]byte(raw))
40+
if err != nil {
41+
t.Fatalf("unexpected error: %v", err)
42+
}
43+
if resp.StatusCode != 200 {
44+
t.Fatalf("expected status 200, got %d", resp.StatusCode)
45+
}
46+
})
47+
48+
t.Run("truncated status line", func(t *testing.T) {
49+
raw := "HTTP/1."
50+
_, err := ParseRawResponse([]byte(raw))
51+
if err == nil {
52+
t.Fatal("expected error for truncated status line")
53+
}
54+
})
55+
56+
t.Run("redirect response no body", func(t *testing.T) {
57+
raw := "HTTP/1.1 302 Found\r\nLocation: https://example.com/new\r\n\r\n"
58+
resp, err := ParseRawResponse([]byte(raw))
59+
if err != nil {
60+
t.Fatalf("unexpected error: %v", err)
61+
}
62+
if resp.StatusCode != 302 {
63+
t.Fatalf("expected status 302, got %d", resp.StatusCode)
64+
}
65+
if loc := resp.Header.Get("Location"); loc != "https://example.com/new" {
66+
t.Fatalf("expected Location https://example.com/new, got %s", loc)
67+
}
68+
})
69+
70+
t.Run("chunked transfer encoding header", func(t *testing.T) {
71+
raw := "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n"
72+
resp, err := ParseRawResponse([]byte(raw))
73+
if err != nil {
74+
t.Fatalf("unexpected error: %v", err)
75+
}
76+
if te := resp.Header.Get("Transfer-Encoding"); te == "" {
77+
// Transfer-Encoding may be consumed by http.ReadResponse, just verify no panic
78+
}
79+
_ = resp
80+
})
81+
82+
t.Run("large header value", func(t *testing.T) {
83+
largeValue := strings.Repeat("A", 8192)
84+
raw := "HTTP/1.1 200 OK\r\nX-Large: " + largeValue + "\r\n\r\n"
85+
resp, err := ParseRawResponse([]byte(raw))
86+
if err != nil {
87+
t.Fatalf("unexpected error: %v", err)
88+
}
89+
if got := resp.Header.Get("X-Large"); got != largeValue {
90+
t.Fatalf("large header value mismatch, got length %d", len(got))
91+
}
92+
})
93+
94+
t.Run("invalid status code", func(t *testing.T) {
95+
raw := "HTTP/1.1 xyz OK\r\n\r\n"
96+
_, err := ParseRawResponse([]byte(raw))
97+
if err == nil {
98+
t.Fatal("expected error for invalid status code")
99+
}
100+
})
101+
102+
t.Run("incomplete header no terminator", func(t *testing.T) {
103+
raw := "HTTP/1.1 200 OK\r\nContent-Type: text/html"
104+
_, err := ParseRawResponse([]byte(raw))
105+
// Should return error or at least not panic
106+
// http.ReadResponse may or may not error on missing \r\n\r\n,
107+
// the key requirement is no panic
108+
_ = err
109+
})
110+
111+
t.Run("binary body", func(t *testing.T) {
112+
body := []byte{0x00, 0x01, 0x02, 0xff, 0xfe, 0xfd}
113+
raw := append([]byte("HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n"), body...)
114+
resp, err := ParseRawResponse(raw)
115+
if err != nil {
116+
t.Fatalf("unexpected error: %v", err)
117+
}
118+
if resp.StatusCode != 200 {
119+
t.Fatalf("expected status 200, got %d", resp.StatusCode)
120+
}
121+
})
122+
}

0 commit comments

Comments
 (0)