sing-box/option/mitm_surge_urlrewrite.go
2025-03-26 23:02:13 +08:00

449 lines
12 KiB
Go

package option
import (
"encoding/base64"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"unicode"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
)
type SurgeURLRewriteLine struct {
Pattern *regexp.Regexp
Destination *url.URL
Redirect bool
Reject bool
}
func (l SurgeURLRewriteLine) String() string {
var fields []string
fields = append(fields, l.Pattern.String())
if l.Reject {
fields = append(fields, "_")
} else {
fields = append(fields, l.Destination.String())
}
switch {
case l.Redirect:
fields = append(fields, "302")
case l.Reject:
fields = append(fields, "reject")
default:
fields = append(fields, "header")
}
return encodeSurgeKeys(fields)
}
func (l SurgeURLRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeURLRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: ", stringValue)
} else if len(fields) < 2 || len(fields) > 3 {
return E.New("invalid surge_url_rewrite line: ", stringValue)
}
pattern, err := regexp.Compile(fields[0].Key)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: invalid pattern: ", stringValue)
}
l.Pattern = pattern
l.Destination, err = url.Parse(fields[1].Key)
if err != nil {
return E.Cause(err, "invalid surge_url_rewrite line: invalid destination: ", stringValue)
}
if len(fields) == 3 {
switch fields[2].Key {
case "header":
case "302":
l.Redirect = true
case "reject":
l.Reject = true
default:
return E.New("invalid surge_url_rewrite line: invalid action: ", stringValue)
}
}
return nil
}
type SurgeHeaderRewriteLine struct {
Response bool
Pattern *regexp.Regexp
Add bool
Delete bool
Replace bool
ReplaceRegex bool
Key string
Match *regexp.Regexp
Value string
}
func (l SurgeHeaderRewriteLine) String() string {
var fields []string
if !l.Response {
fields = append(fields, "http-request")
} else {
fields = append(fields, "http-response")
}
fields = append(fields, l.Pattern.String())
if l.Add {
fields = append(fields, "header-add")
} else if l.Delete {
fields = append(fields, "header-del")
} else if l.Replace {
fields = append(fields, "header-replace")
} else if l.ReplaceRegex {
fields = append(fields, "header-replace-regex")
}
fields = append(fields, l.Key)
if l.Add || l.Replace {
fields = append(fields, l.Value)
} else if l.ReplaceRegex {
fields = append(fields, l.Match.String(), l.Value)
}
return encodeSurgeKeys(fields)
}
func (l SurgeHeaderRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeHeaderRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: ", stringValue)
} else if len(fields) < 4 {
return E.New("invalid surge_header_rewrite line: ", stringValue)
}
switch fields[0].Key {
case "http-request":
case "http-response":
l.Response = true
default:
return E.New("invalid surge_header_rewrite line: invalid type: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[1].Key)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: invalid pattern: ", stringValue)
}
switch fields[2].Key {
case "header-add":
l.Add = true
if len(fields) != 5 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Value = fields[4].Key
case "header-del":
l.Delete = true
l.Key = fields[3].Key
case "header-replace":
l.Replace = true
if len(fields) != 5 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Value = fields[4].Key
case "header-replace-regex":
l.ReplaceRegex = true
if len(fields) != 6 {
return E.New("invalid surge_header_rewrite line: " + stringValue)
}
l.Key = fields[3].Key
l.Match, err = regexp.Compile(fields[4].Key)
if err != nil {
return E.Cause(err, "invalid surge_header_rewrite line: invalid match: ", stringValue)
}
l.Value = fields[5].Key
default:
return E.New("invalid surge_header_rewrite line: invalid action: ", stringValue)
}
return nil
}
type SurgeBodyRewriteLine struct {
Response bool
Pattern *regexp.Regexp
Match []*regexp.Regexp
Replace []string
}
func (l SurgeBodyRewriteLine) String() string {
var fields []string
if !l.Response {
fields = append(fields, "http-request")
} else {
fields = append(fields, "http-response")
}
for i := 0; i < len(l.Match); i += 2 {
fields = append(fields, l.Match[i].String(), l.Replace[i])
}
return strings.Join(fields, " ")
}
func (l SurgeBodyRewriteLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeBodyRewriteLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_body_rewrite line: ", stringValue)
} else if len(fields) < 4 {
return E.New("invalid surge_body_rewrite line: ", stringValue)
} else if len(fields)%2 != 0 {
return E.New("invalid surge_body_rewrite line: ", stringValue)
}
switch fields[0].Key {
case "http-request":
case "http-response":
l.Response = true
default:
return E.New("invalid surge_body_rewrite line: invalid type: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[1].Key)
for i := 2; i < len(fields); i += 2 {
var match *regexp.Regexp
match, err = regexp.Compile(fields[i].Key)
if err != nil {
return E.Cause(err, "invalid surge_body_rewrite line: invalid match: ", stringValue)
}
l.Match = append(l.Match, match)
l.Replace = append(l.Replace, fields[i+1].Key)
}
return nil
}
type SurgeMapLocalLine struct {
Pattern *regexp.Regexp
StatusCode int
File bool
Text bool
TinyGif bool
Base64 bool
Data string
Base64Data []byte
Headers http.Header
}
func (l SurgeMapLocalLine) String() string {
var fields []surgeField
fields = append(fields, surgeField{Key: l.Pattern.String()})
if l.File {
fields = append(fields, surgeField{Key: "data-type", Value: "file"})
fields = append(fields, surgeField{Key: "data", Value: l.Data})
} else if l.Text {
fields = append(fields, surgeField{Key: "data-type", Value: "text"})
fields = append(fields, surgeField{Key: "data", Value: l.Data})
} else if l.TinyGif {
fields = append(fields, surgeField{Key: "data-type", Value: "tiny-gif"})
} else if l.Base64 {
fields = append(fields, surgeField{Key: "data-type", Value: "base64"})
fields = append(fields, surgeField{Key: "data-type", Value: base64.StdEncoding.EncodeToString(l.Base64Data)})
}
if l.StatusCode != 0 {
fields = append(fields, surgeField{Key: "status-code", Value: F.ToString(l.StatusCode), ValueSet: true})
}
if len(l.Headers) > 0 {
var headers []string
for key, values := range l.Headers {
for _, value := range values {
headers = append(headers, key+":"+value)
}
}
fields = append(fields, surgeField{Key: "headers", Value: strings.Join(headers, "|")})
}
return encodeSurgeFields(fields)
}
func (l SurgeMapLocalLine) MarshalJSON() ([]byte, error) {
return json.Marshal(l.String())
}
func (l *SurgeMapLocalLine) UnmarshalJSON(bytes []byte) error {
var stringValue string
err := json.Unmarshal(bytes, &stringValue)
if err != nil {
return err
}
fields, err := surgeFields(stringValue)
if err != nil {
return E.Cause(err, "invalid surge_map_local line: ", stringValue)
} else if len(fields) < 1 {
return E.New("invalid surge_map_local line: ", stringValue)
}
l.Pattern, err = regexp.Compile(fields[0].Key)
if err != nil {
return E.Cause(err, "invalid surge_map_local line: invalid pattern: ", stringValue)
}
dataTypeField := common.Find(fields, func(it surgeField) bool {
return it.Key == "data-type"
})
if !dataTypeField.ValueSet {
return E.New("invalid surge_map_local line: missing data-type: ", stringValue)
}
switch dataTypeField.Value {
case "file":
l.File = true
case "text":
l.Text = true
case "tiny-gif":
l.TinyGif = true
case "base64":
l.Base64 = true
default:
return E.New("unsupported data-type ", dataTypeField.Value)
}
for i := 1; i < len(fields); i++ {
switch fields[i].Key {
case "data-type":
continue
case "data":
if l.File {
l.Data = fields[i].Value
} else if l.Text {
l.Data = fields[i].Value
} else if l.Base64 {
l.Base64Data, err = base64.StdEncoding.DecodeString(fields[i].Value)
if err != nil {
return E.New("invalid surge_map_local line: invalid base64 data: ", stringValue)
}
}
case "status-code":
statusCode, err := strconv.ParseInt(fields[i].Value, 10, 16)
if err != nil {
return E.New("invalid surge_map_local line: invalid status code: ", stringValue)
}
l.StatusCode = int(statusCode)
case "header":
headers := make(http.Header)
for _, headerLine := range strings.Split(fields[i].Value, "|") {
if !strings.Contains(headerLine, ":") {
return E.New("invalid surge_map_local line: headers: missing `:` in item: ", stringValue, ": ", headerLine)
}
headers.Add(common.SubstringBefore(headerLine, ":"), common.SubstringAfter(headerLine, ":"))
}
l.Headers = headers
default:
return E.New("invalid surge_map_local line: unknown options: ", fields[i].Key)
}
}
return nil
}
type surgeField struct {
Key string
Value string
ValueSet bool
}
func encodeSurgeKeys(keys []string) string {
keys = common.Map(keys, func(it string) string {
if strings.ContainsFunc(it, unicode.IsSpace) {
return "\"" + it + "\""
} else {
return it
}
})
return strings.Join(keys, " ")
}
func encodeSurgeFields(fields []surgeField) string {
return strings.Join(common.Map(fields, func(it surgeField) string {
if !it.ValueSet {
if strings.ContainsFunc(it.Key, unicode.IsSpace) {
return "\"" + it.Key + "\""
} else {
return it.Key
}
} else {
if strings.ContainsFunc(it.Value, unicode.IsSpace) {
return it.Key + "=\"" + it.Value + "\""
} else {
return it.Key + "=" + it.Value
}
}
}), " ")
}
func surgeFields(s string) ([]surgeField, error) {
var (
fields []surgeField
currentField *surgeField
)
for _, field := range strings.Fields(s) {
if currentField != nil {
field = " " + field
if strings.HasSuffix(field, "\"") {
field = field[:len(field)-1]
if !currentField.ValueSet {
currentField.Key += field
} else {
currentField.Value += field
}
fields = append(fields, *currentField)
currentField = nil
} else {
if !currentField.ValueSet {
currentField.Key += field
} else {
currentField.Value += field
}
}
continue
}
if !strings.Contains(field, "=") {
if strings.HasPrefix(field, "\"") {
field = field[1:]
if strings.HasSuffix(field, "\"") {
field = field[:len(field)-1]
} else {
currentField = &surgeField{Key: field}
continue
}
}
fields = append(fields, surgeField{Key: field})
} else {
key := common.SubstringBefore(field, "=")
value := common.SubstringAfter(field, "=")
if strings.HasPrefix(value, "\"") {
value = value[1:]
if strings.HasSuffix(field, "\"") {
value = value[:len(value)-1]
} else {
currentField = &surgeField{Key: key, Value: value, ValueSet: true}
continue
}
}
fields = append(fields, surgeField{Key: key, Value: value, ValueSet: true})
}
}
if currentField != nil {
return nil, E.New("invalid surge fields line: ", s)
}
return fields, nil
}