From 5855d79bcf0ed377a78f58f189dcdb2ee29cd9cc Mon Sep 17 00:00:00 2001 From: SashaGonch228 <78559764+SashaGonch228@users.noreply.github.com> Date: Thu, 15 Oct 2026 15:07:30 +0500 Subject: [PATCH] sub: add custom subscription headers and enable gzip compression (#1289) * sub: add support for custom subscription headers * sub: gzip for subs * sub: load and apply custom headers as middleware in initRouter * sub: move header parsing to sub.go, empty default, validate header names and values --------- Co-authored-by: Alireza Ahmadi --- frontend | 2 +- go.mod | 2 +- service/setting.go | 5 +++++ sub/sub.go | 43 ++++++++++++++++++++++++++++++++++++++ sub/sub_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 sub/sub_test.go diff --git a/frontend b/frontend index e452529..80de052 160000 --- a/frontend +++ b/frontend @@ -1 +1 @@ -Subproject commit e4525297b002c1c3be234cc1c9695ef84be741a0 +Subproject commit 80de052a45835b2edb1110ce83e2a947555138f1 diff --git a/go.mod b/go.mod index 07fcbef..6068aef 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/sagernet/sing-vmess v0.2.9-0.20260929152519-9b95ab8c9478 github.com/shirou/gopsutil/v4 v4.26.9 golang.org/x/crypto v0.57.0 + golang.org/x/net v0.58.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10 gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/sqlite v1.6.0 @@ -209,7 +210,6 @@ require ( golang.org/x/arch v0.29.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect golang.org/x/mod v0.41.0 // indirect - golang.org/x/net v0.58.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sync v0.23.0 // indirect golang.org/x/sys v0.48.0 // indirect diff --git a/service/setting.go b/service/setting.go index a47d63f..0d9a957 100644 --- a/service/setting.go +++ b/service/setting.go @@ -91,6 +91,7 @@ var defaultValueMap = map[string]string{ "subURI": "", "subJsonExt": "", "subClashExt": "", + "subCustomHeaders": "", "subClashNoDefGrp": "false", "subClashSprtAll": "false", "subClashUdp": "false", @@ -540,6 +541,10 @@ func (s *SettingService) GetSubClashExt() (string, error) { return s.getString("subClashExt") } +func (s *SettingService) GetSubCustomHeaders() (string, error) { + return s.getString("subCustomHeaders") +} + // GetSubClashNoDefGrp reports whether the default "Proxy"/"Auto" proxy-groups // should never be injected into a Clash subscription. When true, the config is // left with exactly the groups the user defined. diff --git a/sub/sub.go b/sub/sub.go index 7ef29a4..0c0e484 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "strconv" + "strings" "time" "github.com/alireza0/s-ui/config" @@ -16,7 +17,9 @@ import ( "github.com/alireza0/s-ui/service" "github.com/alireza0/s-ui/util/common" + "github.com/gin-contrib/gzip" "github.com/gin-gonic/gin" + "golang.org/x/net/http/httpguts" ) type Server struct { @@ -46,6 +49,20 @@ func (s *Server) initRouter() (*gin.Engine, error) { } engine := gin.Default() + engine.Use(gzip.Gzip(gzip.DefaultCompression)) + + customHeadersStr, err := s.SettingService.GetSubCustomHeaders() + if err != nil { + return nil, err + } + if customHeaders := parseCustomHeaders(customHeadersStr); len(customHeaders) > 0 { + engine.Use(func(c *gin.Context) { + for _, h := range customHeaders { + c.Writer.Header().Set(h.key, h.value) + } + c.Next() + }) + } subPath, err := s.SettingService.GetSubPath() if err != nil { @@ -67,6 +84,32 @@ func (s *Server) initRouter() (*gin.Engine, error) { return engine, nil } +type customHeader struct { + key string + value string +} + +func parseCustomHeaders(raw string) []customHeader { + var headers []customHeader + for _, line := range strings.Split(raw, "\n") { + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") || strings.HasPrefix(line, "//") { + continue + } + key, value, ok := strings.Cut(line, ":") + if !ok { + continue + } + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if value == "" || !httpguts.ValidHeaderFieldName(key) || !httpguts.ValidHeaderFieldValue(value) { + continue + } + headers = append(headers, customHeader{key: key, value: value}) + } + return headers +} + func (s *Server) Start() (err error) { //This is an anonymous function, no function name defer func() { diff --git a/sub/sub_test.go b/sub/sub_test.go new file mode 100644 index 0000000..c0ab7c9 --- /dev/null +++ b/sub/sub_test.go @@ -0,0 +1,52 @@ +package sub + +import ( + "reflect" + "testing" +) + +func TestParseCustomHeaders(t *testing.T) { + testCases := []struct { + name string + in string + want []customHeader + }{ + {"empty string", "", nil}, + {"whitespace only", " \n\t \r\n ", nil}, + {"comment", "# 424242", nil}, + { + "extra spaces", + "\n\tannounce : 42! \n\tannounce-url: t.me/4242\n\t", + []customHeader{{"announce", "42!"}, {"announce-url", "t.me/4242"}}, + }, + { + "values keep their colons", + "Profile-Web-Page-Url: https://sub.example.com:8443/login?redirect=https://a.com", + []customHeader{{"Profile-Web-Page-Url", "https://sub.example.com:8443/login?redirect=https://a.com"}}, + }, + { + "crlf line endings and comments", + "# header 1\r\nX-Custom: value1\r\n// comment 2\r\nX-Other: value2\r\n", + []customHeader{{"X-Custom", "value1"}, {"X-Other", "value2"}}, + }, + { + "malformed lines are skipped", + "random text without colon\n: missing key\nempty value: \n:\nvalid-key: ok\n", + []customHeader{{"valid-key", "ok"}}, + }, + { + // A name an HTTP header cannot carry would go out malformed. + "invalid names are skipped", + "Bad Name: x\nBad(Name): x\nGood-Name: x\n", + []customHeader{{"Good-Name", "x"}}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + if got := parseCustomHeaders(tc.in); !reflect.DeepEqual(got, tc.want) { + t.Errorf("got %+v, want %+v", got, tc.want) + } + }) + } +}