feature: MITM
This commit is contained in:
72
rewrite/base.go
Normal file
72
rewrite/base.go
Normal file
@ -0,0 +1,72 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
var (
|
||||
EmptyDict = NewResponseBody([]byte("{}"))
|
||||
EmptyArray = NewResponseBody([]byte("[]"))
|
||||
OnePixelPNG = NewResponseBody([]byte{0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x11, 0x49, 0x44, 0x41, 0x54, 0x78, 0x9c, 0x62, 0x62, 0x60, 0x60, 0x60, 0x00, 0x04, 0x00, 0x00, 0xff, 0xff, 0x00, 0x0f, 0x00, 0x03, 0xfe, 0x8f, 0xeb, 0xcf, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82})
|
||||
)
|
||||
|
||||
type Body interface {
|
||||
Body() io.ReadCloser
|
||||
ContentLength() int64
|
||||
}
|
||||
|
||||
type ResponseBody struct {
|
||||
data []byte
|
||||
length int64
|
||||
}
|
||||
|
||||
func (r *ResponseBody) Body() io.ReadCloser {
|
||||
return ioutil.NopCloser(bytes.NewReader(r.data))
|
||||
}
|
||||
|
||||
func (r *ResponseBody) ContentLength() int64 {
|
||||
return r.length
|
||||
}
|
||||
|
||||
func NewResponseBody(data []byte) *ResponseBody {
|
||||
return &ResponseBody{
|
||||
data: data,
|
||||
length: int64(len(data)),
|
||||
}
|
||||
}
|
||||
|
||||
type RewriteRules struct {
|
||||
request []C.Rewrite
|
||||
response []C.Rewrite
|
||||
}
|
||||
|
||||
func (rr *RewriteRules) SearchInRequest(do func(C.Rewrite) bool) bool {
|
||||
for _, v := range rr.request {
|
||||
if do(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (rr *RewriteRules) SearchInResponse(do func(C.Rewrite) bool) bool {
|
||||
for _, v := range rr.response {
|
||||
if do(v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func NewRewriteRules(req []C.Rewrite, res []C.Rewrite) *RewriteRules {
|
||||
return &RewriteRules{
|
||||
request: req,
|
||||
response: res,
|
||||
}
|
||||
}
|
||||
|
||||
var _ C.RewriteRule = (*RewriteRules)(nil)
|
202
rewrite/handler.go
Normal file
202
rewrite/handler.go
Normal file
@ -0,0 +1,202 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
"github.com/Dreamacro/clash/listener/mitm"
|
||||
"github.com/Dreamacro/clash/tunnel"
|
||||
)
|
||||
|
||||
var _ mitm.Handler = (*RewriteHandler)(nil)
|
||||
|
||||
type RewriteHandler struct{}
|
||||
|
||||
func (*RewriteHandler) HandleRequest(session *mitm.Session) (*http.Request, *http.Response) {
|
||||
var (
|
||||
request = session.Request()
|
||||
response *http.Response
|
||||
)
|
||||
|
||||
rule, sub, found := matchRewriteRule(request.URL.String(), true)
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
switch rule.RuleType() {
|
||||
case C.MitmReject:
|
||||
response = session.NewResponse(http.StatusNotFound, nil)
|
||||
response.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
case C.MitmReject200:
|
||||
response = session.NewResponse(http.StatusOK, nil)
|
||||
response.Header.Set("Content-Type", "text/html; charset=utf-8")
|
||||
case C.MitmRejectImg:
|
||||
response = session.NewResponse(http.StatusOK, OnePixelPNG.Body())
|
||||
response.Header.Set("Content-Type", "image/png")
|
||||
response.ContentLength = OnePixelPNG.ContentLength()
|
||||
case C.MitmRejectDict:
|
||||
response = session.NewResponse(http.StatusOK, EmptyDict.Body())
|
||||
response.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
response.ContentLength = EmptyDict.ContentLength()
|
||||
case C.MitmRejectArray:
|
||||
response = session.NewResponse(http.StatusOK, EmptyArray.Body())
|
||||
response.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||||
response.ContentLength = EmptyArray.ContentLength()
|
||||
case C.Mitm302:
|
||||
response = session.NewResponse(http.StatusFound, nil)
|
||||
response.Header.Set("Location", rule.ReplaceURLPayload(sub))
|
||||
case C.Mitm307:
|
||||
response = session.NewResponse(http.StatusTemporaryRedirect, nil)
|
||||
response.Header.Set("Location", rule.ReplaceURLPayload(sub))
|
||||
case C.MitmRequestHeader:
|
||||
if len(request.Header) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rawHeader := &bytes.Buffer{}
|
||||
oldHeader := request.Header
|
||||
if err := oldHeader.Write(rawHeader); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
newRawHeader := rule.ReplaceSubPayload(rawHeader.String())
|
||||
tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader)))
|
||||
newHeader, err := tb.ReadMIMEHeader()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil, nil
|
||||
}
|
||||
request.Header = http.Header(newHeader)
|
||||
case C.MitmRequestBody:
|
||||
if !CanRewriteBody(request.ContentLength, request.Header.Get("Content-Type")) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, request.ContentLength)
|
||||
_, err := io.ReadFull(request.Body, buf)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
newBody := rule.ReplaceSubPayload(string(buf))
|
||||
request.Body = io.NopCloser(strings.NewReader(newBody))
|
||||
request.ContentLength = int64(len(newBody))
|
||||
default:
|
||||
found = false
|
||||
}
|
||||
|
||||
if found {
|
||||
if response != nil {
|
||||
response.Close = true
|
||||
}
|
||||
return request, response
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*RewriteHandler) HandleResponse(session *mitm.Session) *http.Response {
|
||||
var (
|
||||
request = session.Request()
|
||||
response = session.Response()
|
||||
)
|
||||
|
||||
rule, _, found := matchRewriteRule(request.URL.String(), false)
|
||||
found = found && rule.RuleRegx() != nil
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch rule.RuleType() {
|
||||
case C.MitmResponseHeader:
|
||||
if len(response.Header) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
rawHeader := &bytes.Buffer{}
|
||||
oldHeader := response.Header
|
||||
if err := oldHeader.Write(rawHeader); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newRawHeader := rule.ReplaceSubPayload(rawHeader.String())
|
||||
tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader)))
|
||||
newHeader, err := tb.ReadMIMEHeader()
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
|
||||
response.Header = http.Header(newHeader)
|
||||
response.Header.Set("Content-Length", strconv.FormatInt(response.ContentLength, 10))
|
||||
case C.MitmResponseBody:
|
||||
if !CanRewriteBody(response.ContentLength, response.Header.Get("Content-Type")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
b, err := mitm.ReadDecompressedBody(response)
|
||||
_ = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
body, err := mitm.DecodeLatin1(bytes.NewReader(b))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
newBody := rule.ReplaceSubPayload(body)
|
||||
|
||||
modifiedBody, err := mitm.EncodeLatin1(newBody)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
response.Body = ioutil.NopCloser(bytes.NewReader(modifiedBody))
|
||||
response.Header.Del("Content-Encoding")
|
||||
response.ContentLength = int64(len(modifiedBody))
|
||||
default:
|
||||
found = false
|
||||
}
|
||||
|
||||
if found {
|
||||
return response
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *RewriteHandler) HandleApiRequest(*mitm.Session) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// HandleError session maybe nil
|
||||
func (h *RewriteHandler) HandleError(*mitm.Session, error) {}
|
||||
|
||||
func matchRewriteRule(url string, isRequest bool) (rr C.Rewrite, sub []string, found bool) {
|
||||
rewrites := tunnel.Rewrites()
|
||||
if isRequest {
|
||||
found = rewrites.SearchInRequest(func(r C.Rewrite) bool {
|
||||
sub = r.URLRegx().FindStringSubmatch(url)
|
||||
if len(sub) != 0 {
|
||||
rr = r
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
found = rewrites.SearchInResponse(func(r C.Rewrite) bool {
|
||||
if r.URLRegx().FindString(url) != "" {
|
||||
rr = r
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
78
rewrite/parser.go
Normal file
78
rewrite/parser.go
Normal file
@ -0,0 +1,78 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
)
|
||||
|
||||
func ParseRewrite(line string) (C.Rewrite, error) {
|
||||
url, others, found := strings.Cut(strings.TrimSpace(line), "url")
|
||||
if !found {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
var (
|
||||
urlRegx *regexp.Regexp
|
||||
ruleType *C.RewriteType
|
||||
ruleRegx *regexp.Regexp
|
||||
rulePayload string
|
||||
|
||||
err error
|
||||
)
|
||||
|
||||
urlRegx, err = regexp.Compile(strings.Trim(url, " "))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
others = strings.Trim(others, " ")
|
||||
first := strings.Split(others, " ")[0]
|
||||
for k, v := range C.RewriteTypeMapping {
|
||||
if k == others {
|
||||
ruleType = &v
|
||||
break
|
||||
}
|
||||
|
||||
if k != first {
|
||||
continue
|
||||
}
|
||||
|
||||
rs := trimArr(strings.Split(others, k))
|
||||
l := len(rs)
|
||||
if l > 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
if l == 1 {
|
||||
ruleType = &v
|
||||
rulePayload = rs[0]
|
||||
break
|
||||
} else {
|
||||
ruleRegx, err = regexp.Compile(rs[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ruleType = &v
|
||||
rulePayload = rs[1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ruleType == nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
return NewRewriteRule(urlRegx, *ruleType, ruleRegx, rulePayload), nil
|
||||
}
|
||||
|
||||
func trimArr(arr []string) (r []string) {
|
||||
for _, e := range arr {
|
||||
if s := strings.Trim(e, " "); s != "" {
|
||||
r = append(r, s)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
56
rewrite/parser_test.go
Normal file
56
rewrite/parser_test.go
Normal file
@ -0,0 +1,56 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestParseRewrite(t *testing.T) {
|
||||
line0 := `^https?://example\.com/resource1/3/ url reject-dict`
|
||||
line1 := `^https?://example\.com/(resource2)/ url 307 https://example.com/new-$1`
|
||||
line2 := `^https?://example\.com/resource4/ url request-header (\r\n)User-Agent:.+(\r\n) request-header $1User-Agent: Fuck-Who$2`
|
||||
line3 := `should be error`
|
||||
|
||||
c0, err0 := ParseRewrite(line0)
|
||||
c1, err1 := ParseRewrite(line1)
|
||||
c2, err2 := ParseRewrite(line2)
|
||||
_, err3 := ParseRewrite(line3)
|
||||
|
||||
assert.NotNil(t, err3)
|
||||
|
||||
assert.Nil(t, err0)
|
||||
assert.Equal(t, c0.RuleType(), constant.MitmRejectDict)
|
||||
|
||||
assert.Nil(t, err1)
|
||||
assert.Equal(t, c1.RuleType(), constant.Mitm307)
|
||||
assert.Equal(t, c1.URLRegx(), regexp.MustCompile(`^https?://example\.com/(resource2)/`))
|
||||
assert.Equal(t, c1.RulePayload(), "https://example.com/new-$1")
|
||||
|
||||
assert.Nil(t, err2)
|
||||
assert.Equal(t, c2.RuleType(), constant.MitmRequestHeader)
|
||||
assert.Equal(t, c2.RuleRegx(), regexp.MustCompile(`(\r\n)User-Agent:.+(\r\n)`))
|
||||
assert.Equal(t, c2.RulePayload(), "$1User-Agent: Fuck-Who$2")
|
||||
}
|
||||
|
||||
func Test1PxPNG(t *testing.T) {
|
||||
m := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||
|
||||
draw.Draw(m, m.Bounds(), &image.Uniform{C: color.Transparent}, image.Point{}, draw.Src)
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
|
||||
assert.Nil(t, png.Encode(buf, m))
|
||||
|
||||
fmt.Printf("len: %d\n", buf.Len())
|
||||
fmt.Printf("% #x\n", buf.Bytes())
|
||||
}
|
89
rewrite/rewrite.go
Normal file
89
rewrite/rewrite.go
Normal file
@ -0,0 +1,89 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
C "github.com/Dreamacro/clash/constant"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
var errInvalid = errors.New("invalid rewrite rule")
|
||||
|
||||
type RewriteRule struct {
|
||||
id string
|
||||
urlRegx *regexp.Regexp
|
||||
ruleType C.RewriteType
|
||||
ruleRegx *regexp.Regexp
|
||||
rulePayload string
|
||||
}
|
||||
|
||||
func (r *RewriteRule) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
func (r *RewriteRule) URLRegx() *regexp.Regexp {
|
||||
return r.urlRegx
|
||||
}
|
||||
|
||||
func (r *RewriteRule) RuleType() C.RewriteType {
|
||||
return r.ruleType
|
||||
}
|
||||
|
||||
func (r *RewriteRule) RuleRegx() *regexp.Regexp {
|
||||
return r.ruleRegx
|
||||
}
|
||||
|
||||
func (r *RewriteRule) RulePayload() string {
|
||||
return r.rulePayload
|
||||
}
|
||||
|
||||
func (r *RewriteRule) ReplaceURLPayload(matchSub []string) string {
|
||||
url := r.rulePayload
|
||||
|
||||
l := len(matchSub)
|
||||
if l < 2 {
|
||||
return url
|
||||
}
|
||||
|
||||
for i := 1; i < l; i++ {
|
||||
url = strings.ReplaceAll(url, "$"+strconv.Itoa(i), matchSub[i])
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func (r *RewriteRule) ReplaceSubPayload(oldData string) string {
|
||||
payload := r.rulePayload
|
||||
if r.ruleRegx == nil {
|
||||
return oldData
|
||||
}
|
||||
|
||||
sub := r.ruleRegx.FindStringSubmatch(oldData)
|
||||
l := len(sub)
|
||||
|
||||
if l == 0 {
|
||||
return oldData
|
||||
}
|
||||
|
||||
for i := 1; i < l; i++ {
|
||||
payload = strings.ReplaceAll(payload, "$"+strconv.Itoa(i), sub[i])
|
||||
}
|
||||
|
||||
return strings.ReplaceAll(oldData, sub[0], payload)
|
||||
}
|
||||
|
||||
func NewRewriteRule(urlRegx *regexp.Regexp, ruleType C.RewriteType, ruleRegx *regexp.Regexp, rulePayload string) *RewriteRule {
|
||||
id, _ := uuid.NewV4()
|
||||
return &RewriteRule{
|
||||
id: id.String(),
|
||||
urlRegx: urlRegx,
|
||||
ruleType: ruleType,
|
||||
ruleRegx: ruleRegx,
|
||||
rulePayload: rulePayload,
|
||||
}
|
||||
}
|
||||
|
||||
var _ C.Rewrite = (*RewriteRule)(nil)
|
28
rewrite/util.go
Normal file
28
rewrite/util.go
Normal file
@ -0,0 +1,28 @@
|
||||
package rewrites
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
var allowContentType = []string{
|
||||
"text/",
|
||||
"application/xhtml",
|
||||
"application/xml",
|
||||
"application/atom+xml",
|
||||
"application/json",
|
||||
"application/x-www-form-urlencoded",
|
||||
}
|
||||
|
||||
func CanRewriteBody(contentLength int64, contentType string) bool {
|
||||
if contentLength <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, v := range allowContentType {
|
||||
if strings.HasPrefix(contentType, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user