mirror of
https://github.com/Alexey71/opera-proxy.git
synced 2026-05-13 14:11:00 +00:00
237 lines
5.3 KiB
Go
237 lines
5.3 KiB
Go
package dialer
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
PROXY_CONNECT_METHOD = "CONNECT"
|
|
PROXY_HOST_HEADER = "Host"
|
|
PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"
|
|
)
|
|
|
|
type stringCb = func() (string, error)
|
|
|
|
type Dialer interface {
|
|
Dial(network, address string) (net.Conn, error)
|
|
}
|
|
|
|
type ContextDialer interface {
|
|
Dialer
|
|
DialContext(ctx context.Context, network, address string) (net.Conn, error)
|
|
}
|
|
|
|
type ProxyDialer struct {
|
|
address stringCb
|
|
tlsServerName stringCb
|
|
fakeSNI stringCb
|
|
auth stringCb
|
|
next ContextDialer
|
|
caPool *x509.CertPool
|
|
}
|
|
|
|
func NewProxyDialer(address, tlsServerName, fakeSNI, auth stringCb, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer {
|
|
return &ProxyDialer{
|
|
address: address,
|
|
tlsServerName: tlsServerName,
|
|
fakeSNI: fakeSNI,
|
|
auth: auth,
|
|
next: nextDialer,
|
|
caPool: caPool,
|
|
}
|
|
}
|
|
|
|
func ProxyDialerFromURL(u *url.URL, next ContextDialer) (*ProxyDialer, error) {
|
|
host := u.Hostname()
|
|
port := u.Port()
|
|
tlsServerName := ""
|
|
var auth stringCb = nil
|
|
|
|
switch strings.ToLower(u.Scheme) {
|
|
case "http":
|
|
if port == "" {
|
|
port = "80"
|
|
}
|
|
case "https":
|
|
if port == "" {
|
|
port = "443"
|
|
}
|
|
tlsServerName = host
|
|
default:
|
|
return nil, errors.New("unsupported proxy type")
|
|
}
|
|
|
|
address := net.JoinHostPort(host, port)
|
|
|
|
if u.User != nil {
|
|
username := u.User.Username()
|
|
password, _ := u.User.Password()
|
|
auth = WrapStringToCb(BasicAuthHeader(username, password))
|
|
}
|
|
return NewProxyDialer(
|
|
WrapStringToCb(address),
|
|
WrapStringToCb(tlsServerName),
|
|
WrapStringToCb(tlsServerName),
|
|
auth,
|
|
nil,
|
|
next), nil
|
|
}
|
|
|
|
func (d *ProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
|
switch network {
|
|
case "tcp", "tcp4", "tcp6":
|
|
default:
|
|
return nil, errors.New("bad network specified for DialContext: only tcp is supported")
|
|
}
|
|
|
|
uAddress, err := d.address()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
conn, err := d.next.DialContext(ctx, "tcp", uAddress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
uTLSServerName, err := d.tlsServerName()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fakeSNI, err := d.fakeSNI()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if uTLSServerName != "" {
|
|
// Custom TLS verification strategy:
|
|
// - Do NOT send SNI in ClientHello (use fakeSNI, may be empty string).
|
|
// - Verify the peer certificate against the real server name using
|
|
// the explicit caPool (Mozilla NSS bundle via bundle.Roots()).
|
|
//
|
|
// No cross-signed intermediate injection needed: bundle.Roots() already
|
|
// contains USERTrust ECC CA as a trusted root, so Go's chain builder
|
|
// resolves Opera's certificate chain without any manual patching.
|
|
conn = tls.Client(conn, &tls.Config{
|
|
ServerName: fakeSNI,
|
|
InsecureSkipVerify: true,
|
|
VerifyConnection: func(cs tls.ConnectionState) error {
|
|
opts := x509.VerifyOptions{
|
|
DNSName: uTLSServerName,
|
|
Intermediates: x509.NewCertPool(),
|
|
Roots: d.caPool,
|
|
}
|
|
for _, cert := range cs.PeerCertificates[1:] {
|
|
opts.Intermediates.AddCert(cert)
|
|
}
|
|
_, err := cs.PeerCertificates[0].Verify(opts)
|
|
return err
|
|
},
|
|
})
|
|
}
|
|
|
|
req := &http.Request{
|
|
Method: PROXY_CONNECT_METHOD,
|
|
Proto: "HTTP/1.1",
|
|
ProtoMajor: 1,
|
|
ProtoMinor: 1,
|
|
RequestURI: address,
|
|
Host: address,
|
|
Header: http.Header{
|
|
PROXY_HOST_HEADER: []string{address},
|
|
},
|
|
}
|
|
|
|
if d.auth != nil {
|
|
auth, err := d.auth()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set(PROXY_AUTHORIZATION_HEADER, auth)
|
|
}
|
|
|
|
rawreq, err := httputil.DumpRequest(req, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = conn.Write(rawreq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proxyResp, err := readResponse(conn, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if proxyResp.StatusCode != http.StatusOK {
|
|
return nil, errors.New(fmt.Sprintf("bad response from upstream proxy server: %s", proxyResp.Status))
|
|
}
|
|
|
|
return conn, nil
|
|
}
|
|
|
|
func (d *ProxyDialer) Dial(network, address string) (net.Conn, error) {
|
|
return d.DialContext(context.Background(), network, address)
|
|
}
|
|
|
|
func (d *ProxyDialer) Address() (string, error) {
|
|
return d.address()
|
|
}
|
|
|
|
// readResponse reads an HTTP/1.1 response from the raw conn after a CONNECT
|
|
// request. It reads byte-by-byte until the \r\n\r\n header terminator is found,
|
|
// then hands the accumulated bytes to http.ReadResponse.
|
|
//
|
|
// Note: byte-by-byte reading is intentional — we must not over-read past the
|
|
// end of headers into the tunneled TLS stream.
|
|
func readResponse(r io.Reader, req *http.Request) (*http.Response, error) {
|
|
endOfResponse := []byte("\r\n\r\n")
|
|
buf := &bytes.Buffer{}
|
|
b := make([]byte, 1)
|
|
for {
|
|
n, err := r.Read(b)
|
|
if n < 1 && err == nil {
|
|
continue
|
|
}
|
|
|
|
buf.Write(b)
|
|
sl := buf.Bytes()
|
|
if len(sl) < len(endOfResponse) {
|
|
continue
|
|
}
|
|
|
|
if bytes.Equal(sl[len(sl)-4:], endOfResponse) {
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return http.ReadResponse(bufio.NewReader(buf), req)
|
|
}
|
|
|
|
func BasicAuthHeader(login, password string) string {
|
|
return "Basic " + base64.StdEncoding.EncodeToString(
|
|
[]byte(login+":"+password))
|
|
}
|
|
|
|
func WrapStringToCb(s string) func() (string, error) {
|
|
return func() (string, error) {
|
|
return s, nil
|
|
}
|
|
}
|