github.com/yaling888/clash@v1.53.0/mitm/handler.go (about)

     1  package mitm
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"io"
     8  	"net/http"
     9  	"net/textproto"
    10  	"strconv"
    11  	"strings"
    12  
    13  	C "github.com/yaling888/clash/constant"
    14  	"github.com/yaling888/clash/tunnel"
    15  )
    16  
    17  var _ C.RewriteHandler = (*RewriteHandler)(nil)
    18  
    19  type RewriteHandler struct{}
    20  
    21  func (*RewriteHandler) HandleRequest(session *C.MitmSession) (*http.Request, *http.Response) {
    22  	var (
    23  		request  = session.Request
    24  		response *http.Response
    25  	)
    26  
    27  	rule, sub, found := matchRewriteRule(request.URL.String(), true)
    28  	if !found {
    29  		return nil, nil
    30  	}
    31  
    32  	switch rule.RuleType() {
    33  	case C.MitmReject:
    34  		response = session.NewResponse(http.StatusNotFound, nil)
    35  		response.Header.Set("Content-Type", "text/html; charset=UTF-8")
    36  	case C.MitmReject200:
    37  		var payload string
    38  		if len(rule.RulePayload()) > 0 {
    39  			payload = rule.RulePayload()[0]
    40  		}
    41  		response = session.NewResponse(http.StatusOK, nil)
    42  		if payload != "" {
    43  			if strings.Contains(payload, "{") {
    44  				response.Header.Set("Content-Type", "application/json; charset=UTF-8")
    45  			} else {
    46  				response.Header.Set("Content-Type", "text/plain; charset=UTF-8")
    47  			}
    48  			response.Body = io.NopCloser(strings.NewReader(payload))
    49  			response.ContentLength = int64(len(payload))
    50  		} else {
    51  			response.Header.Set("Content-Type", "text/html; charset=UTF-8")
    52  		}
    53  	case C.MitmReject204:
    54  		response = session.NewResponse(http.StatusNoContent, nil)
    55  		response.Header.Set("Content-Type", "text/html; charset=UTF-8")
    56  	case C.MitmRejectImg:
    57  		response = session.NewResponse(http.StatusOK, OnePixelPNG.Body())
    58  		response.Header.Set("Content-Type", "image/png")
    59  		response.ContentLength = OnePixelPNG.ContentLength()
    60  	case C.MitmRejectDict:
    61  		response = session.NewResponse(http.StatusOK, EmptyDict.Body())
    62  		response.Header.Set("Content-Type", "application/json; charset=UTF-8")
    63  		response.ContentLength = EmptyDict.ContentLength()
    64  	case C.MitmRejectArray:
    65  		response = session.NewResponse(http.StatusOK, EmptyArray.Body())
    66  		response.Header.Set("Content-Type", "application/json; charset=UTF-8")
    67  		response.ContentLength = EmptyArray.ContentLength()
    68  	case C.Mitm302:
    69  		response = session.NewResponse(http.StatusFound, nil)
    70  		response.Header.Set("Location", rule.ReplaceURLPayload(sub))
    71  	case C.Mitm307:
    72  		response = session.NewResponse(http.StatusTemporaryRedirect, nil)
    73  		response.Header.Set("Location", rule.ReplaceURLPayload(sub))
    74  	case C.MitmRequestHeader:
    75  		if len(request.Header) == 0 {
    76  			return nil, nil
    77  		}
    78  
    79  		rawHeader := &bytes.Buffer{}
    80  		oldHeader := request.Header
    81  		if err := oldHeader.Write(rawHeader); err != nil {
    82  			return nil, nil
    83  		}
    84  
    85  		newRawHeader, ok := rule.ReplaceSubPayload(rawHeader.String())
    86  		if !ok {
    87  			return nil, nil
    88  		}
    89  
    90  		tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader)))
    91  		newHeader, err := tb.ReadMIMEHeader()
    92  		if err != nil && !errors.Is(err, io.EOF) {
    93  			return nil, nil
    94  		}
    95  		request.Header = http.Header(newHeader)
    96  	case C.MitmRequestBody:
    97  		if !CanRewriteBody(request.ContentLength, "", request.Header.Get("Content-Type")) {
    98  			return nil, nil
    99  		}
   100  
   101  		buf := make([]byte, request.ContentLength)
   102  		_, err := io.ReadFull(request.Body, buf)
   103  		if err != nil {
   104  			return nil, nil
   105  		}
   106  
   107  		newBody, _ := rule.ReplaceSubPayload(string(buf))
   108  		request.Body = io.NopCloser(strings.NewReader(newBody))
   109  		request.ContentLength = int64(len(newBody))
   110  	default:
   111  		found = false
   112  	}
   113  
   114  	if found {
   115  		if response != nil {
   116  			response.Close = true
   117  		}
   118  		return request, response
   119  	}
   120  	return nil, nil
   121  }
   122  
   123  func (*RewriteHandler) HandleResponse(session *C.MitmSession) *http.Response {
   124  	var (
   125  		request  = session.Request
   126  		response = session.Response
   127  	)
   128  
   129  	rule, _, found := matchRewriteRule(request.URL.String(), false)
   130  	found = found && rule.RuleRegx() != nil
   131  	if !found {
   132  		return nil
   133  	}
   134  
   135  	switch rule.RuleType() {
   136  	case C.MitmResponseHeader:
   137  		if len(response.Header) == 0 {
   138  			return nil
   139  		}
   140  
   141  		rawHeader := &bytes.Buffer{}
   142  		oldHeader := response.Header
   143  		if err := oldHeader.Write(rawHeader); err != nil {
   144  			return nil
   145  		}
   146  
   147  		newRawHeader, ok := rule.ReplaceSubPayload(rawHeader.String())
   148  		if !ok {
   149  			return nil
   150  		}
   151  
   152  		tb := textproto.NewReader(bufio.NewReader(strings.NewReader(newRawHeader)))
   153  		newHeader, err := tb.ReadMIMEHeader()
   154  		if err != nil && !errors.Is(err, io.EOF) {
   155  			return nil
   156  		}
   157  
   158  		response.Header = http.Header(newHeader)
   159  		response.Header.Set("Content-Length", strconv.FormatInt(response.ContentLength, 10))
   160  	case C.MitmResponseBody:
   161  		contentType := response.Header.Get("Content-Type")
   162  		if !CanRewriteBody(response.ContentLength, response.Header.Get("Content-Encoding"), contentType) {
   163  			return nil
   164  		}
   165  
   166  		b, err := C.ReadDecompressedBody(response)
   167  		_ = response.Body.Close()
   168  		if err != nil {
   169  			return nil
   170  		}
   171  
   172  		body := ""
   173  		isUTF8 := strings.HasSuffix(strings.ToUpper(contentType), "UTF-8")
   174  		if isUTF8 {
   175  			body = string(b)
   176  		} else {
   177  			body, err = C.DecodeLatin1(bytes.NewReader(b))
   178  			if err != nil {
   179  				return nil
   180  			}
   181  		}
   182  
   183  		newBody, _ := rule.ReplaceSubPayload(body)
   184  
   185  		var modifiedBody []byte
   186  		if isUTF8 {
   187  			modifiedBody = []byte(newBody)
   188  		} else {
   189  			modifiedBody, err = C.EncodeLatin1(newBody)
   190  			if err != nil {
   191  				return nil
   192  			}
   193  		}
   194  
   195  		response.Body = io.NopCloser(bytes.NewReader(modifiedBody))
   196  		response.ContentLength = int64(len(modifiedBody))
   197  		response.Header.Del("Content-Encoding")
   198  		response.Header.Set("Content-Length", strconv.FormatInt(response.ContentLength, 10))
   199  	default:
   200  		found = false
   201  	}
   202  
   203  	if found {
   204  		return response
   205  	}
   206  	return nil
   207  }
   208  
   209  func (h *RewriteHandler) HandleApiRequest(*C.MitmSession) bool {
   210  	return false
   211  }
   212  
   213  // HandleError session maybe nil
   214  func (h *RewriteHandler) HandleError(*C.MitmSession, error) {}
   215  
   216  func matchRewriteRule(url string, isRequest bool) (rr C.Rewrite, sub []string, found bool) {
   217  	rewrites := tunnel.Rewrites()
   218  	if isRequest {
   219  		found = rewrites.SearchInRequest(func(r C.Rewrite) bool {
   220  			// sub = r.URLRegx().FindStringSubmatch(url) // std
   221  			sub = findStringSubmatch(r.URLRegx(), url)
   222  			if len(sub) != 0 {
   223  				rr = r
   224  				return true
   225  			}
   226  			return false
   227  		})
   228  	} else {
   229  		found = rewrites.SearchInResponse(func(r C.Rewrite) bool {
   230  			// if r.URLRegx().FindString(url) != "" { // std
   231  			if m, _ := r.URLRegx().MatchString(url); m {
   232  				rr = r
   233  				return true
   234  			}
   235  			return false
   236  		})
   237  	}
   238  
   239  	return
   240  }