mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 11:15:03 +03:00
fix(sub): bound what one subscription response renders
A subscription's size is the product of its clients and inbounds with each one's remark and endpoints, and nothing bounded that product: data adopted from a node could make one unauthenticated request render gigabytes and run the panel out of memory. Each request now carries an 8 MiB budget that the raw, JSON and Clash outputs charge as they materialize a client's links, configs or proxies. Rendering stops once it is spent, with one warning, and anything below the budget is unchanged.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
|
||||
)
|
||||
|
||||
// maxRenderedBytes bounds what one subscription response renders: every client and
|
||||
// inbound of the subId repeats the remark and endpoints, so the size is a product.
|
||||
const maxRenderedBytes = 8 << 20
|
||||
|
||||
// renderBudget counts rendered bytes against maxRenderedBytes. The zero value
|
||||
// is a fresh budget.
|
||||
type renderBudget int
|
||||
|
||||
// take books n more bytes and reports whether they still fit. The caller drops
|
||||
// what does not fit and stops rendering: a spent budget admits nothing else.
|
||||
func (b *renderBudget) take(n int) bool {
|
||||
if b.spent() {
|
||||
return false
|
||||
}
|
||||
*b += renderBudget(n)
|
||||
if b.spent() {
|
||||
logger.Warningf("sub: rendering stopped at %d bytes, the remaining links are dropped", maxRenderedBytes)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *renderBudget) spent() bool {
|
||||
return *b > maxRenderedBytes
|
||||
}
|
||||
|
||||
// keepLink books a rendered raw link, and its line break, against the request's budget.
|
||||
func (s *SubService) keepLink(link string) bool {
|
||||
return s.rendered.take(len(link) + 1)
|
||||
}
|
||||
|
||||
// budgetedConfig renders a client's JSON configs, keeping those the request's budget admits.
|
||||
func (s *SubJsonService) budgetedConfig(subReq *SubService, inbound *model.Inbound, client model.Client, host string) []json_util.RawMessage {
|
||||
if subReq.rendered.spent() {
|
||||
return nil
|
||||
}
|
||||
configs := s.getConfig(subReq, inbound, client, host)
|
||||
for i, config := range configs {
|
||||
if !subReq.rendered.take(len(config)) {
|
||||
return configs[:i]
|
||||
}
|
||||
}
|
||||
return configs
|
||||
}
|
||||
|
||||
// budgetedProxies renders a client's Clash proxies, keeping those the request's budget admits;
|
||||
// the YAML is written only at the end, and its JSON form is about as long.
|
||||
func (s *SubClashService) budgetedProxies(subReq *SubService, inbound *model.Inbound, client model.Client, host string) []map[string]any {
|
||||
if subReq.rendered.spent() {
|
||||
return nil
|
||||
}
|
||||
proxies := s.getProxies(subReq, inbound, client, host)
|
||||
for i, proxy := range proxies {
|
||||
text, _ := json.Marshal(proxy)
|
||||
if !subReq.rendered.take(len(text)) {
|
||||
return proxies[:i]
|
||||
}
|
||||
}
|
||||
return proxies
|
||||
}
|
||||
Reference in New Issue
Block a user