karmada/pkg/util/proxy/upgrade_dialer.go

272 lines
7.6 KiB
Go

/*
Copyright 2015 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package proxy
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"net/http/httputil"
"net/url"
"time"
"golang.org/x/net/proxy"
utilnet "k8s.io/apimachinery/pkg/util/net"
"k8s.io/apimachinery/third_party/forked/golang/netutil"
)
// UpgradeDialer knows how to upgrade an HTTP request to one that supports
// multiplexed streams. After Dial() is invoked, Conn will be usable.
type UpgradeDialer struct {
//tlsConfig holds the TLS configuration settings to use when connecting
//to the remote server.
tlsConfig *tls.Config
// Dialer is the dialer used to connect. Used if non-nil.
Dialer *net.Dialer
// header holds the HTTP request headers for dialing.
header http.Header
// proxier knows which proxy to use given a request.
proxier func(req *http.Request) (*url.URL, error)
// pingPeriod is a period for sending Ping frames over established
// connections.
pingPeriod time.Duration
}
var _ utilnet.TLSClientConfigHolder = &UpgradeDialer{}
var _ utilnet.Dialer = &UpgradeDialer{}
// NewUpgradeDialerWithConfig creates a new UpgradeRoundTripper with the specified
// configuration.
func NewUpgradeDialerWithConfig(cfg UpgradeDialerWithConfig) *UpgradeDialer {
if cfg.Proxier == nil {
cfg.Proxier = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
}
return &UpgradeDialer{
tlsConfig: cfg.TLS,
proxier: cfg.Proxier,
pingPeriod: cfg.PingPeriod,
header: cfg.Header,
}
}
// UpgradeDialerWithConfig is a set of options for an UpgradeDialer.
type UpgradeDialerWithConfig struct {
// TLS configuration used by the round tripper.
TLS *tls.Config
// Header holds the HTTP request headers for dialing. Optional.
Header http.Header
// Proxier is a proxy function invoked on each request. Optional.
Proxier func(*http.Request) (*url.URL, error)
// PingPeriod is a period for sending Pings on the connection.
// Optional.
PingPeriod time.Duration
}
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder for proper TLS checking during
// proxying with a roundtripper.
func (u *UpgradeDialer) TLSClientConfig() *tls.Config {
return u.tlsConfig
}
// Dial implements k8s.io/apimachinery/pkg/util/net.Dialer.
func (u *UpgradeDialer) Dial(req *http.Request) (net.Conn, error) {
conn, err := u.dial(req)
if err != nil {
return nil, err
}
if err := req.Write(conn); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}
// dial dials the host specified by req, using TLS if appropriate.
func (u *UpgradeDialer) dial(req *http.Request) (net.Conn, error) {
proxyURL, err := u.proxier(req)
if err != nil {
return nil, err
}
if proxyURL == nil {
return u.dialWithoutProxy(req.Context(), req.URL)
}
switch proxyURL.Scheme {
case "socks5":
return u.dialWithSocks5Proxy(req, proxyURL)
case "https", "http", "":
return u.dialWithHTTPProxy(req, proxyURL)
}
return nil, fmt.Errorf("proxy URL scheme not supported: %s", proxyURL.Scheme)
}
// dialWithHTTPProxy dials the host specified by url through an http or an https proxy.
func (u *UpgradeDialer) dialWithHTTPProxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) {
// ensure we use a canonical host with proxyReq
targetHost := netutil.CanonicalAddr(req.URL)
// proxying logic adapted from http://blog.h6t.eu/post/74098062923/golang-websocket-with-http-proxy-support
proxyReq := http.Request{
Method: "CONNECT",
URL: &url.URL{},
Host: targetHost,
Header: u.header,
}
proxyReq = *proxyReq.WithContext(req.Context())
if pa := u.proxyAuth(proxyURL); pa != "" {
proxyReq.Header.Set("Proxy-Authorization", pa)
}
proxyDialConn, err := u.dialWithoutProxy(proxyReq.Context(), proxyURL)
if err != nil {
return nil, err
}
//nolint:staticcheck // SA1019 ignore deprecated httputil.NewProxyClientConn
proxyClientConn := httputil.NewProxyClientConn(proxyDialConn, nil)
response, err := proxyClientConn.Do(&proxyReq)
//nolint:staticcheck // SA1019 ignore deprecated httputil.ErrPersistEOF: it might be
// returned from the invocation of proxyClientConn.Do
if err != nil && err != httputil.ErrPersistEOF {
return nil, err
}
if response != nil && response.StatusCode >= 300 || response.StatusCode < 200 {
return nil, fmt.Errorf("CONNECT request to %s returned response: %s", proxyURL.Redacted(), response.Status)
}
rwc, _ := proxyClientConn.Hijack()
if req.URL.Scheme == "https" {
return u.tlsConn(proxyReq.Context(), rwc, targetHost)
}
return rwc, nil
}
// dialWithSocks5Proxy dials the host specified by url through a socks5 proxy.
func (u *UpgradeDialer) dialWithSocks5Proxy(req *http.Request, proxyURL *url.URL) (net.Conn, error) {
// ensure we use a canonical host with proxyReq
targetHost := netutil.CanonicalAddr(req.URL)
proxyDialAddr := netutil.CanonicalAddr(proxyURL)
var auth *proxy.Auth
if proxyURL.User != nil {
pass, _ := proxyURL.User.Password()
auth = &proxy.Auth{
User: proxyURL.User.Username(),
Password: pass,
}
}
dialer := u.Dialer
if dialer == nil {
dialer = &net.Dialer{
Timeout: 30 * time.Second,
}
}
proxyDialer, err := proxy.SOCKS5("tcp", proxyDialAddr, auth, dialer)
if err != nil {
return nil, err
}
// According to the implementation of proxy.SOCKS5, the type assertion will always succeed
contextDialer, ok := proxyDialer.(proxy.ContextDialer)
if !ok {
return nil, errors.New("SOCKS5 Dialer must implement ContextDialer")
}
proxyDialConn, err := contextDialer.DialContext(req.Context(), "tcp", targetHost)
if err != nil {
return nil, err
}
if req.URL.Scheme == "https" {
return u.tlsConn(req.Context(), proxyDialConn, targetHost)
}
return proxyDialConn, nil
}
// tlsConn returns a TLS client side connection using rwc as the underlying transport.
func (u *UpgradeDialer) tlsConn(ctx context.Context, rwc net.Conn, targetHost string) (net.Conn, error) {
host, _, err := net.SplitHostPort(targetHost)
if err != nil {
return nil, err
}
tlsConfig := u.tlsConfig
switch {
case tlsConfig == nil:
tlsConfig = &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12}
case len(tlsConfig.ServerName) == 0:
tlsConfig = tlsConfig.Clone()
tlsConfig.ServerName = host
}
tlsConn := tls.Client(rwc, tlsConfig)
if err := tlsConn.HandshakeContext(ctx); err != nil {
tlsConn.Close()
return nil, err
}
return tlsConn, nil
}
// dialWithoutProxy dials the host specified by url, using TLS if appropriate.
func (u *UpgradeDialer) dialWithoutProxy(ctx context.Context, url *url.URL) (net.Conn, error) {
dialAddr := netutil.CanonicalAddr(url)
dialer := u.Dialer
if dialer == nil {
dialer = &net.Dialer{}
}
if url.Scheme == "http" {
return dialer.DialContext(ctx, "tcp", dialAddr)
}
tlsDialer := tls.Dialer{
NetDialer: dialer,
Config: u.tlsConfig,
}
return tlsDialer.DialContext(ctx, "tcp", dialAddr)
}
// proxyAuth returns, for a given proxy URL, the value to be used for the Proxy-Authorization header
func (u *UpgradeDialer) proxyAuth(proxyURL *url.URL) string {
if proxyURL == nil || proxyURL.User == nil {
return ""
}
username := proxyURL.User.Username()
password, _ := proxyURL.User.Password()
auth := username + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
}