mirror of
https://github.com/apernet/hysteria.git
synced 2025-04-03 04:27:39 +03:00
190 lines
4.5 KiB
Go
190 lines
4.5 KiB
Go
package outbounds
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
httpRequestTimeout = 10 * time.Second
|
|
)
|
|
|
|
var (
|
|
errHTTPUDPNotSupported = errors.New("UDP not supported by HTTP proxy")
|
|
errHTTPUnsupportedScheme = errors.New("unsupported scheme for HTTP proxy (use http:// or https://)")
|
|
)
|
|
|
|
type errHTTPRequestFailed struct {
|
|
Status int
|
|
}
|
|
|
|
func (e errHTTPRequestFailed) Error() string {
|
|
return fmt.Sprintf("HTTP request failed: %d", e.Status)
|
|
}
|
|
|
|
// httpOutbound is a PluggableOutbound that connects to the target using
|
|
// an HTTP/HTTPS proxy server (that supports the CONNECT method).
|
|
// HTTP proxies don't support UDP by design, so this outbound will reject
|
|
// any UDP request with errHTTPUDPNotSupported.
|
|
// Since HTTP proxies support using either IP or domain name as the target
|
|
// address, it will ignore ResolveInfo in AddrEx and always only use Host.
|
|
type httpOutbound struct {
|
|
Dialer *net.Dialer
|
|
Addr string
|
|
HTTPS bool
|
|
Insecure bool
|
|
ServerName string
|
|
BasicAuth string // This is after Base64 encoding
|
|
}
|
|
|
|
func NewHTTPOutbound(proxyURL string, insecure bool) (PluggableOutbound, error) {
|
|
u, err := url.Parse(proxyURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if u.Scheme != "http" && u.Scheme != "https" {
|
|
return nil, errHTTPUnsupportedScheme
|
|
}
|
|
addr := u.Host
|
|
if u.Port() == "" {
|
|
if u.Scheme == "http" {
|
|
addr = net.JoinHostPort(u.Host, "80")
|
|
} else {
|
|
addr = net.JoinHostPort(u.Host, "443")
|
|
}
|
|
}
|
|
var basicAuth string
|
|
if u.User != nil {
|
|
username := u.User.Username()
|
|
password, _ := u.User.Password()
|
|
basicAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(username+":"+password))
|
|
}
|
|
return &httpOutbound{
|
|
Dialer: &net.Dialer{Timeout: defaultDialerTimeout},
|
|
Addr: addr,
|
|
HTTPS: u.Scheme == "https",
|
|
Insecure: insecure,
|
|
ServerName: u.Hostname(),
|
|
BasicAuth: basicAuth,
|
|
}, nil
|
|
}
|
|
|
|
func (o *httpOutbound) dial() (net.Conn, error) {
|
|
conn, err := o.Dialer.Dial("tcp", o.Addr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if o.HTTPS {
|
|
// Wrap the connection with TLS if the proxy is HTTPS.
|
|
conn = tls.Client(conn, &tls.Config{
|
|
InsecureSkipVerify: o.Insecure,
|
|
ServerName: o.Addr,
|
|
})
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
func (o *httpOutbound) addrExToRequest(reqAddr *AddrEx) (*http.Request, error) {
|
|
req := &http.Request{
|
|
Method: http.MethodConnect,
|
|
URL: &url.URL{
|
|
Host: net.JoinHostPort(reqAddr.Host, strconv.Itoa(int(reqAddr.Port))),
|
|
},
|
|
Header: http.Header{
|
|
"Proxy-Connection": []string{"Keep-Alive"},
|
|
},
|
|
}
|
|
if o.BasicAuth != "" {
|
|
req.Header.Add("Proxy-Authorization", o.BasicAuth)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (o *httpOutbound) TCP(reqAddr *AddrEx) (net.Conn, error) {
|
|
req, err := o.addrExToRequest(reqAddr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := o.dial()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := req.Write(conn); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if err := conn.SetDeadline(time.Now().Add(httpRequestTimeout)); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
bufReader := bufio.NewReader(conn)
|
|
resp, err := http.ReadResponse(bufReader, req)
|
|
if resp != nil {
|
|
// Don't need response body here.
|
|
_ = resp.Body.Close()
|
|
}
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
_ = conn.Close()
|
|
return nil, errHTTPRequestFailed{resp.StatusCode}
|
|
}
|
|
if err := conn.SetDeadline(time.Time{}); err != nil {
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
if bufReader.Buffered() > 0 {
|
|
// There is still data in the buffered reader.
|
|
// We need to get it out and put it into a cachedConn,
|
|
// so that handleConnect can read it.
|
|
data := make([]byte, bufReader.Buffered())
|
|
_, err := io.ReadFull(bufReader, data)
|
|
if err != nil {
|
|
// Read from buffer failed, is this possible?
|
|
_ = conn.Close()
|
|
return nil, err
|
|
}
|
|
cachedConn := &cachedConn{
|
|
Conn: conn,
|
|
Buffer: *bytes.NewBuffer(data),
|
|
}
|
|
return cachedConn, nil
|
|
} else {
|
|
return conn, nil
|
|
}
|
|
}
|
|
|
|
func (o *httpOutbound) UDP(reqAddr *AddrEx) (UDPConn, error) {
|
|
return nil, errHTTPUDPNotSupported
|
|
}
|
|
|
|
// cachedConn is a net.Conn wrapper that first Read()s from a buffer,
|
|
// and then from the underlying net.Conn when the buffer is drained.
|
|
type cachedConn struct {
|
|
net.Conn
|
|
Buffer bytes.Buffer
|
|
}
|
|
|
|
func (c *cachedConn) Read(b []byte) (int, error) {
|
|
if c.Buffer.Len() > 0 {
|
|
n, err := c.Buffer.Read(b)
|
|
if err == io.EOF {
|
|
// Buffer is drained, hide it from the caller
|
|
err = nil
|
|
}
|
|
return n, err
|
|
}
|
|
return c.Conn.Read(b)
|
|
}
|