fix(sub): add content disposition for subscriptions

This commit is contained in:
phj233 2026-06-27 16:26:56 +08:00
parent deb285d8bc
commit 32ca517187
2 changed files with 95 additions and 0 deletions

View file

@ -1,6 +1,10 @@
package sub
import (
"fmt"
"net/url"
"strings"
"github.com/alireza0/s-ui/logger"
"github.com/alireza0/s-ui/service"
@ -75,4 +79,33 @@ func (s *SubHandler) addHeaders(c *gin.Context, headers []string) {
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2])
c.Writer.Header().Set("Content-Disposition", contentDispositionHeader(headers[2]))
}
func contentDispositionHeader(name string) string {
filename := strings.TrimSpace(name)
if filename == "" {
filename = "subscription"
}
return fmt.Sprintf("attachment; filename=\"%s\"; filename*=UTF-8''%s", asciiSafeFilename(filename), url.PathEscape(filename))
}
func asciiSafeFilename(filename string) string {
var builder strings.Builder
for _, r := range filename {
switch {
case r == '"' || r == '\\':
builder.WriteByte('_')
case r >= 0x20 && r <= 0x7e:
builder.WriteRune(r)
}
}
fallback := strings.TrimSpace(builder.String())
if fallback == "" {
return "subscription"
}
return fallback
}

62
sub/subHandler_test.go Normal file
View file

@ -0,0 +1,62 @@
package sub
import (
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func TestAddHeadersPreservesSubscriptionHeadersAndAddsContentDisposition(t *testing.T) {
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
handler := &SubHandler{}
handler.addHeaders(context, []string{
"upload=1; download=2; total=3; expire=4",
"12",
"phj233_vpn",
})
headers := recorder.Header()
tests := map[string]string{
"Subscription-Userinfo": "upload=1; download=2; total=3; expire=4",
"Profile-Update-Interval": "12",
"Profile-Title": "phj233_vpn",
"Content-Disposition": "attachment; filename=\"phj233_vpn\"; filename*=UTF-8''phj233_vpn",
}
for key, want := range tests {
if got := headers.Get(key); got != want {
t.Fatalf("header %s = %q, want %q", key, got, want)
}
}
}
func TestContentDispositionHeaderUsesSubscriptionNameWithoutExtension(t *testing.T) {
got := contentDispositionHeader("phj233_vpn")
want := "attachment; filename=\"phj233_vpn\"; filename*=UTF-8''phj233_vpn"
if got != want {
t.Fatalf("contentDispositionHeader() = %q, want %q", got, want)
}
}
func TestContentDispositionHeaderEscapesUTF8Name(t *testing.T) {
got := contentDispositionHeader("蓝胖云 LanPangYun")
want := "attachment; filename=\"LanPangYun\"; filename*=UTF-8''%E8%93%9D%E8%83%96%E4%BA%91%20LanPangYun"
if got != want {
t.Fatalf("contentDispositionHeader() = %q, want %q", got, want)
}
}
func TestContentDispositionHeaderFallsBackWhenNameIsEmpty(t *testing.T) {
got := contentDispositionHeader(" ")
want := "attachment; filename=\"subscription\"; filename*=UTF-8''subscription"
if got != want {
t.Fatalf("contentDispositionHeader() = %q, want %q", got, want)
}
}