mirror of
https://github.com/alireza0/s-ui.git
synced 2026-10-20 19:02:09 +00:00
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 <alireza7@gmail.com>
This commit is contained in:
parent
0af0d37ab5
commit
5855d79bcf
5 changed files with 102 additions and 2 deletions
2
frontend
2
frontend
|
|
@ -1 +1 @@
|
|||
Subproject commit e4525297b002c1c3be234cc1c9695ef84be741a0
|
||||
Subproject commit 80de052a45835b2edb1110ce83e2a947555138f1
|
||||
2
go.mod
2
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
43
sub/sub.go
43
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() {
|
||||
|
|
|
|||
52
sub/sub_test.go
Normal file
52
sub/sub_test.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue