Files
opera-proxy/dialer/upstream.go
T

237 lines
5.3 KiB
Go
Raw Normal View History

2024-11-03 15:17:29 +02:00
package dialer
2021-03-26 22:34:43 +02:00
import (
"bufio"
"bytes"
"context"
"crypto/tls"
"crypto/x509"
2024-11-03 15:17:29 +02:00
"encoding/base64"
2021-03-26 22:34:43 +02:00
"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"
)
2024-11-03 15:17:29 +02:00
type stringCb = func() (string, error)
2021-03-26 22:34:43 +02:00
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 {
2026-01-08 22:04:07 +02:00
address stringCb
tlsServerName stringCb
fakeSNI stringCb
auth stringCb
next ContextDialer
caPool *x509.CertPool
2021-03-26 22:34:43 +02:00
}
2026-01-08 22:04:07 +02:00
func NewProxyDialer(address, tlsServerName, fakeSNI, auth stringCb, caPool *x509.CertPool, nextDialer ContextDialer) *ProxyDialer {
2021-03-26 22:34:43 +02:00
return &ProxyDialer{
2026-01-08 22:04:07 +02:00
address: address,
tlsServerName: tlsServerName,
fakeSNI: fakeSNI,
auth: auth,
next: nextDialer,
caPool: caPool,
2021-03-26 22:34:43 +02:00
}
}
func ProxyDialerFromURL(u *url.URL, next ContextDialer) (*ProxyDialer, error) {
host := u.Hostname()
port := u.Port()
tlsServerName := ""
2024-11-03 15:17:29 +02:00
var auth stringCb = nil
2021-03-26 22:34:43 +02:00
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()
2024-11-03 15:17:29 +02:00
auth = WrapStringToCb(BasicAuthHeader(username, password))
2021-03-26 22:34:43 +02:00
}
2024-12-21 20:47:28 +02:00
return NewProxyDialer(
WrapStringToCb(address),
WrapStringToCb(tlsServerName),
WrapStringToCb(tlsServerName),
auth,
nil,
next), nil
2021-03-26 22:34:43 +02:00
}
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")
}
2024-11-03 15:17:29 +02:00
uAddress, err := d.address()
if err != nil {
return nil, err
}
conn, err := d.next.DialContext(ctx, "tcp", uAddress)
2021-03-26 22:34:43 +02:00
if err != nil {
return nil, err
}
2024-11-03 15:17:29 +02:00
uTLSServerName, err := d.tlsServerName()
if err != nil {
return nil, err
}
2024-12-21 20:47:28 +02:00
fakeSNI, err := d.fakeSNI()
if err != nil {
return nil, err
}
2024-11-03 15:17:29 +02:00
if uTLSServerName != "" {
2026-04-30 19:04:28 +03:00
// 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.
2021-03-26 22:34:43 +02:00
conn = tls.Client(conn, &tls.Config{
2024-12-21 20:47:28 +02:00
ServerName: fakeSNI,
2021-03-26 22:34:43 +02:00
InsecureSkipVerify: true,
VerifyConnection: func(cs tls.ConnectionState) error {
opts := x509.VerifyOptions{
2024-11-03 15:17:29 +02:00
DNSName: uTLSServerName,
2021-03-26 22:34:43 +02:00
Intermediates: x509.NewCertPool(),
Roots: d.caPool,
2021-03-26 22:34:43 +02:00
}
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 {
2024-11-03 15:17:29 +02:00
auth, err := d.auth()
if err != nil {
return nil, err
}
req.Header.Set(PROXY_AUTHORIZATION_HEADER, auth)
2021-03-26 22:34:43 +02:00
}
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)
}
2025-09-14 22:56:19 +03:00
func (d *ProxyDialer) Address() (string, error) {
return d.address()
}
2026-04-30 19:04:28 +03:00
// 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.
2021-03-26 22:34:43 +02:00
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)
}
2024-11-03 15:17:29 +02:00
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
}
}