2023-12-20 12:23:59 +00:00
|
|
|
/*
|
|
|
|
Copyright 2023 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 websocket
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
2024-05-15 06:54:18 +00:00
|
|
|
"errors"
|
2023-12-20 12:23:59 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
gwebsocket "github.com/gorilla/websocket"
|
|
|
|
|
|
|
|
"k8s.io/apimachinery/pkg/util/httpstream"
|
2024-05-15 06:54:18 +00:00
|
|
|
"k8s.io/apimachinery/pkg/util/httpstream/wsstream"
|
2023-12-20 12:23:59 +00:00
|
|
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
|
|
|
restclient "k8s.io/client-go/rest"
|
|
|
|
"k8s.io/client-go/transport"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
_ utilnet.TLSClientConfigHolder = &RoundTripper{}
|
|
|
|
_ http.RoundTripper = &RoundTripper{}
|
|
|
|
)
|
|
|
|
|
|
|
|
// ConnectionHolder defines functions for structure providing
|
|
|
|
// access to the websocket connection.
|
|
|
|
type ConnectionHolder interface {
|
|
|
|
DataBufferSize() int
|
|
|
|
Connection() *gwebsocket.Conn
|
|
|
|
}
|
|
|
|
|
|
|
|
// RoundTripper knows how to establish a connection to a remote WebSocket endpoint and make it available for use.
|
|
|
|
// RoundTripper must not be reused.
|
|
|
|
type RoundTripper struct {
|
|
|
|
// TLSConfig holds the TLS configuration settings to use when connecting
|
|
|
|
// to the remote server.
|
|
|
|
TLSConfig *tls.Config
|
|
|
|
|
|
|
|
// Proxier specifies a function to return a proxy for a given
|
|
|
|
// Request. If the function returns a non-nil error, the
|
|
|
|
// request is aborted with the provided error.
|
|
|
|
// If Proxy is nil or returns a nil *URL, no proxy is used.
|
|
|
|
Proxier func(req *http.Request) (*url.URL, error)
|
|
|
|
|
|
|
|
// Conn holds the WebSocket connection after a round trip.
|
|
|
|
Conn *gwebsocket.Conn
|
|
|
|
}
|
|
|
|
|
|
|
|
// Connection returns the stored websocket connection.
|
|
|
|
func (rt *RoundTripper) Connection() *gwebsocket.Conn {
|
|
|
|
return rt.Conn
|
|
|
|
}
|
|
|
|
|
|
|
|
// DataBufferSize returns the size of buffers for the
|
|
|
|
// websocket connection.
|
|
|
|
func (rt *RoundTripper) DataBufferSize() int {
|
|
|
|
return 32 * 1024
|
|
|
|
}
|
|
|
|
|
|
|
|
// TLSClientConfig implements pkg/util/net.TLSClientConfigHolder.
|
|
|
|
func (rt *RoundTripper) TLSClientConfig() *tls.Config {
|
|
|
|
return rt.TLSConfig
|
|
|
|
}
|
|
|
|
|
|
|
|
// RoundTrip connects to the remote websocket using the headers in the request and the TLS
|
|
|
|
// configuration from the config
|
|
|
|
func (rt *RoundTripper) RoundTrip(request *http.Request) (retResp *http.Response, retErr error) {
|
|
|
|
defer func() {
|
|
|
|
if request.Body != nil {
|
|
|
|
err := request.Body.Close()
|
|
|
|
if retErr == nil {
|
|
|
|
retErr = err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
// set the protocol version directly on the dialer from the header
|
2024-05-15 06:54:18 +00:00
|
|
|
protocolVersions := request.Header[wsstream.WebSocketProtocolHeader]
|
|
|
|
delete(request.Header, wsstream.WebSocketProtocolHeader)
|
2023-12-20 12:23:59 +00:00
|
|
|
|
|
|
|
dialer := gwebsocket.Dialer{
|
|
|
|
Proxy: rt.Proxier,
|
|
|
|
TLSClientConfig: rt.TLSConfig,
|
|
|
|
Subprotocols: protocolVersions,
|
|
|
|
ReadBufferSize: rt.DataBufferSize() + 1024, // add space for the protocol byte indicating which channel the data is for
|
|
|
|
WriteBufferSize: rt.DataBufferSize() + 1024, // add space for the protocol byte indicating which channel the data is for
|
|
|
|
}
|
|
|
|
switch request.URL.Scheme {
|
|
|
|
case "https":
|
|
|
|
request.URL.Scheme = "wss"
|
|
|
|
case "http":
|
|
|
|
request.URL.Scheme = "ws"
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("unknown url scheme: %s", request.URL.Scheme)
|
|
|
|
}
|
|
|
|
wsConn, resp, err := dialer.DialContext(request.Context(), request.URL.String(), request.Header)
|
|
|
|
if err != nil {
|
2024-05-15 06:54:18 +00:00
|
|
|
if errors.Is(err, gwebsocket.ErrBadHandshake) {
|
2024-08-19 08:01:33 +00:00
|
|
|
// Enhance the error message with the response status if possible.
|
|
|
|
if resp != nil && len(resp.Status) > 0 {
|
|
|
|
err = fmt.Errorf("%w (%s)", err, resp.Status)
|
|
|
|
}
|
2024-05-15 06:54:18 +00:00
|
|
|
return nil, &httpstream.UpgradeFailureError{Cause: err}
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ensure we got back a protocol we understand
|
|
|
|
foundProtocol := false
|
|
|
|
for _, protocolVersion := range protocolVersions {
|
|
|
|
if protocolVersion == wsConn.Subprotocol() {
|
|
|
|
foundProtocol = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !foundProtocol {
|
|
|
|
wsConn.Close() // nolint:errcheck
|
|
|
|
return nil, &httpstream.UpgradeFailureError{Cause: fmt.Errorf("invalid protocol, expected one of %q, got %q", protocolVersions, wsConn.Subprotocol())}
|
2023-12-20 12:23:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
rt.Conn = wsConn
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// RoundTripperFor transforms the passed rest config into a wrapped roundtripper, as well
|
|
|
|
// as a pointer to the websocket RoundTripper. The websocket RoundTripper contains the
|
|
|
|
// websocket connection after RoundTrip() on the wrapper. Returns an error if there is
|
|
|
|
// a problem creating the round trippers.
|
|
|
|
func RoundTripperFor(config *restclient.Config) (http.RoundTripper, ConnectionHolder, error) {
|
|
|
|
transportCfg, err := config.TransportConfig()
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
tlsConfig, err := transport.TLSConfigFor(transportCfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
proxy := config.Proxy
|
|
|
|
if proxy == nil {
|
|
|
|
proxy = utilnet.NewProxierWithNoProxyCIDR(http.ProxyFromEnvironment)
|
|
|
|
}
|
|
|
|
|
|
|
|
upgradeRoundTripper := &RoundTripper{
|
|
|
|
TLSConfig: tlsConfig,
|
|
|
|
Proxier: proxy,
|
|
|
|
}
|
|
|
|
wrapper, err := transport.HTTPWrappersForConfig(transportCfg, upgradeRoundTripper)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
return wrapper, upgradeRoundTripper, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Negotiate opens a connection to a remote server and attempts to negotiate
|
|
|
|
// a WebSocket connection. Upon success, it returns the negotiated connection.
|
|
|
|
// The round tripper rt must use the WebSocket round tripper wsRt - see RoundTripperFor.
|
|
|
|
func Negotiate(rt http.RoundTripper, connectionInfo ConnectionHolder, req *http.Request, protocols ...string) (*gwebsocket.Conn, error) {
|
2024-05-15 06:54:18 +00:00
|
|
|
// Plumb protocols to RoundTripper#RoundTrip
|
|
|
|
req.Header[wsstream.WebSocketProtocolHeader] = protocols
|
2023-12-20 12:23:59 +00:00
|
|
|
resp, err := rt.RoundTrip(req)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
err = resp.Body.Close()
|
|
|
|
if err != nil {
|
|
|
|
connectionInfo.Connection().Close()
|
|
|
|
return nil, fmt.Errorf("error closing response body: %v", err)
|
|
|
|
}
|
|
|
|
return connectionInfo.Connection(), nil
|
|
|
|
}
|