github.com/metacubex/mihomo@v1.18.5/adapter/outbound/http.go (about)

     1  package outbound
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"crypto/tls"
     7  	"encoding/base64"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"io"
    12  	"net"
    13  	"net/http"
    14  	"strconv"
    15  
    16  	N "github.com/metacubex/mihomo/common/net"
    17  	"github.com/metacubex/mihomo/component/ca"
    18  	"github.com/metacubex/mihomo/component/dialer"
    19  	"github.com/metacubex/mihomo/component/proxydialer"
    20  	C "github.com/metacubex/mihomo/constant"
    21  )
    22  
    23  type Http struct {
    24  	*Base
    25  	user      string
    26  	pass      string
    27  	tlsConfig *tls.Config
    28  	option    *HttpOption
    29  }
    30  
    31  type HttpOption struct {
    32  	BasicOption
    33  	Name           string            `proxy:"name"`
    34  	Server         string            `proxy:"server"`
    35  	Port           int               `proxy:"port"`
    36  	UserName       string            `proxy:"username,omitempty"`
    37  	Password       string            `proxy:"password,omitempty"`
    38  	TLS            bool              `proxy:"tls,omitempty"`
    39  	SNI            string            `proxy:"sni,omitempty"`
    40  	SkipCertVerify bool              `proxy:"skip-cert-verify,omitempty"`
    41  	Fingerprint    string            `proxy:"fingerprint,omitempty"`
    42  	Headers        map[string]string `proxy:"headers,omitempty"`
    43  }
    44  
    45  // StreamConnContext implements C.ProxyAdapter
    46  func (h *Http) StreamConnContext(ctx context.Context, c net.Conn, metadata *C.Metadata) (net.Conn, error) {
    47  	if h.tlsConfig != nil {
    48  		cc := tls.Client(c, h.tlsConfig)
    49  		err := cc.HandshakeContext(ctx)
    50  		c = cc
    51  		if err != nil {
    52  			return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
    53  		}
    54  	}
    55  
    56  	if err := h.shakeHand(metadata, c); err != nil {
    57  		return nil, err
    58  	}
    59  	return c, nil
    60  }
    61  
    62  // DialContext implements C.ProxyAdapter
    63  func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) {
    64  	return h.DialContextWithDialer(ctx, dialer.NewDialer(h.Base.DialOptions(opts...)...), metadata)
    65  }
    66  
    67  // DialContextWithDialer implements C.ProxyAdapter
    68  func (h *Http) DialContextWithDialer(ctx context.Context, dialer C.Dialer, metadata *C.Metadata) (_ C.Conn, err error) {
    69  	if len(h.option.DialerProxy) > 0 {
    70  		dialer, err = proxydialer.NewByName(h.option.DialerProxy, dialer)
    71  		if err != nil {
    72  			return nil, err
    73  		}
    74  	}
    75  	c, err := dialer.DialContext(ctx, "tcp", h.addr)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("%s connect error: %w", h.addr, err)
    78  	}
    79  	N.TCPKeepAlive(c)
    80  
    81  	defer func(c net.Conn) {
    82  		safeConnClose(c, err)
    83  	}(c)
    84  
    85  	c, err = h.StreamConnContext(ctx, c, metadata)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  
    90  	return NewConn(c, h), nil
    91  }
    92  
    93  // SupportWithDialer implements C.ProxyAdapter
    94  func (h *Http) SupportWithDialer() C.NetWork {
    95  	return C.TCP
    96  }
    97  
    98  func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error {
    99  	addr := metadata.RemoteAddress()
   100  	HeaderString := "CONNECT " + addr + " HTTP/1.1\r\n"
   101  	tempHeaders := map[string]string{
   102  		"Host":             addr,
   103  		"User-Agent":       "Go-http-client/1.1",
   104  		"Proxy-Connection": "Keep-Alive",
   105  	}
   106  
   107  	for key, value := range h.option.Headers {
   108  		tempHeaders[key] = value
   109  	}
   110  
   111  	if h.user != "" && h.pass != "" {
   112  		auth := h.user + ":" + h.pass
   113  		tempHeaders["Proxy-Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
   114  	}
   115  
   116  	for key, value := range tempHeaders {
   117  		HeaderString += key + ": " + value + "\r\n"
   118  	}
   119  
   120  	HeaderString += "\r\n"
   121  
   122  	_, err := rw.Write([]byte(HeaderString))
   123  
   124  	if err != nil {
   125  		return err
   126  	}
   127  
   128  	resp, err := http.ReadResponse(bufio.NewReader(rw), nil)
   129  
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	if resp.StatusCode == http.StatusOK {
   135  		return nil
   136  	}
   137  
   138  	if resp.StatusCode == http.StatusProxyAuthRequired {
   139  		return errors.New("HTTP need auth")
   140  	}
   141  
   142  	if resp.StatusCode == http.StatusMethodNotAllowed {
   143  		return errors.New("CONNECT method not allowed by proxy")
   144  	}
   145  
   146  	if resp.StatusCode >= http.StatusInternalServerError {
   147  		return errors.New(resp.Status)
   148  	}
   149  
   150  	return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode)
   151  }
   152  
   153  func NewHttp(option HttpOption) (*Http, error) {
   154  	var tlsConfig *tls.Config
   155  	if option.TLS {
   156  		sni := option.Server
   157  		if option.SNI != "" {
   158  			sni = option.SNI
   159  		}
   160  		var err error
   161  		tlsConfig, err = ca.GetSpecifiedFingerprintTLSConfig(&tls.Config{
   162  			InsecureSkipVerify: option.SkipCertVerify,
   163  			ServerName:         sni,
   164  		}, option.Fingerprint)
   165  		if err != nil {
   166  			return nil, err
   167  		}
   168  	}
   169  
   170  	return &Http{
   171  		Base: &Base{
   172  			name:   option.Name,
   173  			addr:   net.JoinHostPort(option.Server, strconv.Itoa(option.Port)),
   174  			tp:     C.Http,
   175  			tfo:    option.TFO,
   176  			mpTcp:  option.MPTCP,
   177  			iface:  option.Interface,
   178  			rmark:  option.RoutingMark,
   179  			prefer: C.NewDNSPrefer(option.IPVersion),
   180  		},
   181  		user:      option.UserName,
   182  		pass:      option.Password,
   183  		tlsConfig: tlsConfig,
   184  		option:    &option,
   185  	}, nil
   186  }