272 lines
7.6 KiB
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))
|
|
}
|