From e124f9f881d2ca9fac4145b5565f4b24810f687d Mon Sep 17 00:00:00 2001 From: shiweikang Date: Sat, 11 Apr 2026 12:43:05 +0800 Subject: [PATCH 1/2] Proxy: Fix nil pointer panic in BuildStreamURL and resource leak in ParseBody 1. BuildStreamURL: net.ParseIP() returns nil for non-IP hostnames (e.g., "example.com"), then calling nil.To4() panics. Add nil check before calling To4(). 2. ParseBody: defer r.Close() is placed after the ReadAll error check. If ReadAll fails, the function returns early without closing r, causing a resource leak. Move defer to the top of the function. --- internal/utils/utils.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/utils/utils.go b/internal/utils/utils.go index c031c0e59..fe32a4143 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -69,11 +69,12 @@ func ApiCORS(ctx context.Context, w http.ResponseWriter, r *http.Request) bool { // ParseBody read the body from r, and unmarshal JSON to v. func ParseBody(r io.ReadCloser, v interface{}) error { + defer r.Close() + b, err := io.ReadAll(r) if err != nil { return errors.Wrapf(err, "read body") } - defer r.Close() if len(b) == 0 { return nil @@ -97,7 +98,7 @@ func BuildStreamURL(r string) (string, error) { defaultVhost := !strings.Contains(u.Hostname(), ".") // If hostname is actually an IP address, it's __defaultVhost__. - if ip := net.ParseIP(u.Hostname()); ip.To4() != nil { + if ip := net.ParseIP(u.Hostname()); ip != nil && ip.To4() != nil { defaultVhost = true } From df7a5addb8f2b51b4b0043aa650300d59b455f78 Mon Sep 17 00:00:00 2001 From: Jacob Su Date: Tue, 19 May 2026 11:32:14 +0800 Subject: [PATCH 2/2] Proxy: Add UT coverage for nil pointer and resource leak fixes Add TestParseBody_CloseCalledOnReadError to verify r.Close() is called even when ReadAll fails (resource leak fix). Enhance TestBuildStreamURL with: - Comment explaining the example.com case would panic before the nil pointer fix (net.ParseIP returns nil for non-IP hostnames) - IPv6 test cases to verify ip.To4() check works correctly - Clarifying comments for each test case category --- internal/utils/utils_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index bab316628..e67b7e12b 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -121,6 +121,19 @@ func TestParseBody_ReadError(t *testing.T) { } } +// TestParseBody_CloseCalledOnReadError verifies that r.Close() is called even +// when ReadAll fails - this prevents the resource leak fixed by moving defer +// to the top of the function. +func TestParseBody_CloseCalledOnReadError(t *testing.T) { + rc := &errReadCloser{} + if err := ParseBody(rc, &struct{}{}); err == nil { + t.Fatal("want error") + } + if !rc.closed { + t.Fatal("Close() was not called on read error - resource leak") + } +} + func TestParseBody_UnmarshalError(t *testing.T) { var v struct{ Name string } err := ParseBody(io.NopCloser(strings.NewReader("not json")), &v) @@ -136,11 +149,20 @@ func TestBuildStreamURL(t *testing.T) { cases := []struct { in, want string }{ + // Domain names with dots use hostname as vhost. + // This case would panic with nil pointer dereference before the fix + // because net.ParseIP("example.com") returns nil and nil.To4() panics. {"rtmp://example.com/live/stream", "example.com/live/stream"}, {"rtmp://example.com:1935/live/stream", "example.com/live/stream"}, + // IPv4 addresses use defaultVhost. {"rtmp://127.0.0.1/live/stream", "__defaultVhost__/live/stream"}, + // Hostnames without dots use defaultVhost. {"rtmp://localhost/live/stream", "__defaultVhost__/live/stream"}, {"rtmp://localhost:1935/live/stream", "__defaultVhost__/live/stream"}, + // IPv6 addresses: net.ParseIP returns non-nil but To4() returns nil, + // but they still get defaultVhost because they contain no dots. + {"rtmp://[::1]/live/stream", "__defaultVhost__/live/stream"}, + {"rtmp://[2001:db8::1]:1935/live/stream", "__defaultVhost__/live/stream"}, } for _, c := range cases { got, err := BuildStreamURL(c.in)