github.com/chwjbn/xclash@v0.2.0/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 "io" 11 "net" 12 "net/http" 13 "net/url" 14 "strconv" 15 16 "github.com/chwjbn/xclash/component/dialer" 17 C "github.com/chwjbn/xclash/constant" 18 ) 19 20 type Http struct { 21 *Base 22 user string 23 pass string 24 tlsConfig *tls.Config 25 } 26 27 type HttpOption struct { 28 BasicOption 29 Name string `proxy:"name"` 30 Server string `proxy:"server"` 31 Port int `proxy:"port"` 32 UserName string `proxy:"username,omitempty"` 33 Password string `proxy:"password,omitempty"` 34 TLS bool `proxy:"tls,omitempty"` 35 SNI string `proxy:"sni,omitempty"` 36 SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` 37 } 38 39 // StreamConn implements C.ProxyAdapter 40 func (h *Http) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { 41 if h.tlsConfig != nil { 42 cc := tls.Client(c, h.tlsConfig) 43 err := cc.Handshake() 44 c = cc 45 if err != nil { 46 return nil, fmt.Errorf("%s connect error: %w", h.addr, err) 47 } 48 } 49 50 if err := h.shakeHand(metadata, c); err != nil { 51 return nil, err 52 } 53 return c, nil 54 } 55 56 // DialContext implements C.ProxyAdapter 57 func (h *Http) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { 58 c, err := dialer.DialContext(ctx, "tcp", h.addr, h.Base.DialOptions(opts...)...) 59 if err != nil { 60 return nil, fmt.Errorf("%s connect error: %w", h.addr, err) 61 } 62 tcpKeepAlive(c) 63 64 defer safeConnClose(c, err) 65 66 c, err = h.StreamConn(c, metadata) 67 if err != nil { 68 return nil, err 69 } 70 71 return NewConn(c, h), nil 72 } 73 74 func (h *Http) shakeHand(metadata *C.Metadata, rw io.ReadWriter) error { 75 addr := metadata.RemoteAddress() 76 req := &http.Request{ 77 Method: http.MethodConnect, 78 URL: &url.URL{ 79 Host: addr, 80 }, 81 Host: addr, 82 Header: http.Header{ 83 "Proxy-Connection": []string{"Keep-Alive"}, 84 }, 85 } 86 87 if h.user != "" && h.pass != "" { 88 auth := h.user + ":" + h.pass 89 req.Header.Add("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) 90 } 91 92 if err := req.Write(rw); err != nil { 93 return err 94 } 95 96 resp, err := http.ReadResponse(bufio.NewReader(rw), req) 97 if err != nil { 98 return err 99 } 100 101 if resp.StatusCode == http.StatusOK { 102 return nil 103 } 104 105 if resp.StatusCode == http.StatusProxyAuthRequired { 106 return errors.New("HTTP need auth") 107 } 108 109 if resp.StatusCode == http.StatusMethodNotAllowed { 110 return errors.New("CONNECT method not allowed by proxy") 111 } 112 113 if resp.StatusCode >= http.StatusInternalServerError { 114 return errors.New(resp.Status) 115 } 116 117 return fmt.Errorf("can not connect remote err code: %d", resp.StatusCode) 118 } 119 120 func NewHttp(option HttpOption) *Http { 121 var tlsConfig *tls.Config 122 if option.TLS { 123 sni := option.Server 124 if option.SNI != "" { 125 sni = option.SNI 126 } 127 tlsConfig = &tls.Config{ 128 InsecureSkipVerify: option.SkipCertVerify, 129 ServerName: sni, 130 } 131 } 132 133 return &Http{ 134 Base: &Base{ 135 name: option.Name, 136 addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), 137 tp: C.Http, 138 iface: option.Interface, 139 }, 140 user: option.UserName, 141 pass: option.Password, 142 tlsConfig: tlsConfig, 143 } 144 }