mirror of
https://github.com/rclone/rclone.git
synced 2026-11-08 02:32:57 -05:00
HTTPConnectDial built the Basic credential from url.Userinfo.String(), which percent-encodes the user name and password. A proxy URL such as http://me%40example.com:p%40ss@proxy:3128 sent "me%40example.com:p%40ss" instead of "me@example.com:p@ss" and the proxy refused it. Use Username() and Password() like net/http does, which also always puts the ":" separator in.
122 lines
3.6 KiB
Go
122 lines
3.6 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// maxResponseBytes is the maximum size of CONNECT response we will
|
|
// read from the proxy so a malicious proxy can't use all our memory.
|
|
const maxResponseBytes = 1024 * 1024
|
|
|
|
// bufferedConn is a net.Conn which reads from buffered first then Conn
|
|
type bufferedConn struct {
|
|
net.Conn
|
|
buffered []byte // unread bytes received after the CONNECT response
|
|
}
|
|
|
|
// Read from buffered first then the underlying Conn
|
|
func (c *bufferedConn) Read(p []byte) (n int, err error) {
|
|
if len(c.buffered) > 0 {
|
|
n = copy(p, c.buffered)
|
|
c.buffered = c.buffered[n:]
|
|
return n, nil
|
|
}
|
|
return c.Conn.Read(p)
|
|
}
|
|
|
|
// HTTPConnectDial connects using HTTP CONNECT via proxyDialer
|
|
//
|
|
// It will read the HTTP proxy address from the environment in the
|
|
// standard way.
|
|
//
|
|
// It optionally takes a proxyDialer to dial the HTTP proxy server.
|
|
// If nil is passed, it will use the default net.Dialer.
|
|
func HTTPConnectDial(network, addr string, proxyURL *url.URL, proxyDialer proxy.Dialer) (net.Conn, error) {
|
|
if proxyDialer == nil {
|
|
proxyDialer = &net.Dialer{}
|
|
}
|
|
if proxyURL == nil {
|
|
return proxyDialer.Dial(network, addr)
|
|
}
|
|
|
|
// prepare proxy host with default ports
|
|
host := proxyURL.Host
|
|
if !strings.Contains(host, ":") {
|
|
if strings.EqualFold(proxyURL.Scheme, "https") {
|
|
host += ":443"
|
|
} else {
|
|
host += ":80"
|
|
}
|
|
}
|
|
|
|
// connect to proxy
|
|
conn, err := proxyDialer.Dial(network, host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed to Dial: %q", err)
|
|
}
|
|
|
|
// wrap TLS if HTTPS proxy
|
|
if strings.EqualFold(proxyURL.Scheme, "https") {
|
|
tlsConfig := &tls.Config{ServerName: proxyURL.Hostname()}
|
|
tlsConn := tls.Client(conn, tlsConfig)
|
|
if err := tlsConn.Handshake(); err != nil {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed to make TLS connection: %q", err)
|
|
}
|
|
conn = tlsConn
|
|
}
|
|
|
|
// send CONNECT
|
|
user := proxyURL.User
|
|
if user != nil {
|
|
// Use the decoded user name and password - user.String() would
|
|
// send them percent-encoded (eg "me%40example.com").
|
|
password, _ := user.Password()
|
|
credential := base64.StdEncoding.EncodeToString([]byte(user.Username() + ":" + password))
|
|
_, err = fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\nProxy-Authorization: Basic %s\r\n\r\n", addr, addr, credential)
|
|
} else {
|
|
_, err = fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, addr)
|
|
}
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed to send CONNECT: %q", err)
|
|
}
|
|
limitedConn := &io.LimitedReader{R: conn, N: maxResponseBytes}
|
|
br := bufio.NewReader(limitedConn)
|
|
req := &http.Request{URL: &url.URL{Scheme: "http", Host: addr}}
|
|
resp, err := http.ReadResponse(br, req)
|
|
if err != nil {
|
|
_ = conn.Close()
|
|
if limitedConn.N <= 0 {
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy response too large (more than %d bytes)", maxResponseBytes)
|
|
}
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed to read response: %q", err)
|
|
}
|
|
if resp.StatusCode != http.StatusOK {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed: %s", resp.Status)
|
|
}
|
|
// The server may have sent bytes for the tunnelled protocol (eg an
|
|
// SSH banner or FTP greeting) which br has buffered along with the
|
|
// CONNECT response - make sure they aren't lost.
|
|
if n := br.Buffered(); n > 0 {
|
|
buffered := make([]byte, n)
|
|
if _, err := io.ReadFull(br, buffered); err != nil {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("HTTP CONNECT proxy failed to read buffered bytes: %q", err)
|
|
}
|
|
conn = &bufferedConn{Conn: conn, buffered: buffered}
|
|
}
|
|
return conn, nil
|
|
}
|