fix(subscription): bound outbound response body (#5493)

This commit is contained in:
n0ctal
2026-06-23 13:48:01 +05:00
committed by GitHub
parent 67344cae6f
commit ecb0b0a9fa
2 changed files with 46 additions and 1 deletions
+20 -1
View File
@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -18,6 +19,24 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/util/link" "github.com/mhsanaei/3x-ui/v3/internal/util/link"
) )
// maxOutboundSubscriptionBytes caps a single outbound subscription response.
// It is larger than the 2 MiB user-facing subscription cap because an outbound
// subscription may aggregate many upstream outbounds into one document.
const maxOutboundSubscriptionBytes int64 = 8 << 20
var errOutboundSubscriptionBodyTooLarge = errors.New("outbound subscription response body exceeds size limit")
func readBoundedOutboundSubscriptionBody(r io.Reader) ([]byte, error) {
body, err := io.ReadAll(io.LimitReader(r, maxOutboundSubscriptionBytes+1))
if err != nil {
return nil, err
}
if int64(len(body)) > maxOutboundSubscriptionBytes {
return nil, fmt.Errorf("%w (limit: %d bytes)", errOutboundSubscriptionBodyTooLarge, maxOutboundSubscriptionBytes)
}
return body, nil
}
// OutboundSubscriptionService manages remote outbound subscriptions. // OutboundSubscriptionService manages remote outbound subscriptions.
type OutboundSubscriptionService struct { type OutboundSubscriptionService struct {
settingService SettingService settingService SettingService
@@ -281,7 +300,7 @@ func (s *OutboundSubscriptionService) fetchAndStore(sub *model.OutboundSubscript
s.recordError(sub, err) s.recordError(sub, err)
return nil, err return nil, err
} }
body, err := io.ReadAll(resp.Body) body, err := readBoundedOutboundSubscriptionBody(resp.Body)
if err != nil { if err != nil {
s.recordError(sub, err) s.recordError(sub, err)
return nil, err return nil, err
@@ -1,12 +1,38 @@
package service package service
import ( import (
"bytes"
"errors"
"testing" "testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model" "github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/util/link" "github.com/mhsanaei/3x-ui/v3/internal/util/link"
) )
func TestReadBoundedOutboundSubscriptionBody(t *testing.T) {
t.Run("accepts body at the limit", func(t *testing.T) {
want := bytes.Repeat([]byte("a"), int(maxOutboundSubscriptionBytes))
got, err := readBoundedOutboundSubscriptionBody(bytes.NewReader(want))
if err != nil {
t.Fatalf("readBoundedOutboundSubscriptionBody: %v", err)
}
if !bytes.Equal(got, want) {
t.Fatalf("body mismatch: got %d bytes, want %d", len(got), len(want))
}
})
t.Run("rejects body over the limit", func(t *testing.T) {
body := bytes.Repeat([]byte("b"), int(maxOutboundSubscriptionBytes)+1)
got, err := readBoundedOutboundSubscriptionBody(bytes.NewReader(body))
if !errors.Is(err, errOutboundSubscriptionBodyTooLarge) {
t.Fatalf("error = %v, want errOutboundSubscriptionBodyTooLarge", err)
}
if got != nil {
t.Fatalf("oversized body returned %d bytes, want nil", len(got))
}
})
}
func TestDefaultPrefixNumber(t *testing.T) { func TestDefaultPrefixNumber(t *testing.T) {
mk := func(id int, prefix string) *model.OutboundSubscription { mk := func(id int, prefix string) *model.OutboundSubscription {
return &model.OutboundSubscription{Id: id, TagPrefix: prefix} return &model.OutboundSubscription{Id: id, TagPrefix: prefix}