1/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package remotecommand
18
19import (
20	"fmt"
21	"io"
22	"net/http"
23	"net/url"
24
25	"k8s.io/klog"
26
27	"k8s.io/apimachinery/pkg/util/httpstream"
28	"k8s.io/apimachinery/pkg/util/remotecommand"
29	restclient "k8s.io/client-go/rest"
30	spdy "k8s.io/client-go/transport/spdy"
31)
32
33// StreamOptions holds information pertaining to the current streaming session:
34// input/output streams, if the client is requesting a TTY, and a terminal size queue to
35// support terminal resizing.
36type StreamOptions struct {
37	Stdin             io.Reader
38	Stdout            io.Writer
39	Stderr            io.Writer
40	Tty               bool
41	TerminalSizeQueue TerminalSizeQueue
42}
43
44// Executor is an interface for transporting shell-style streams.
45type Executor interface {
46	// Stream initiates the transport of the standard shell streams. It will transport any
47	// non-nil stream to a remote system, and return an error if a problem occurs. If tty
48	// is set, the stderr stream is not used (raw TTY manages stdout and stderr over the
49	// stdout stream).
50	Stream(options StreamOptions) error
51}
52
53type streamCreator interface {
54	CreateStream(headers http.Header) (httpstream.Stream, error)
55}
56
57type streamProtocolHandler interface {
58	stream(conn streamCreator) error
59}
60
61// streamExecutor handles transporting standard shell streams over an httpstream connection.
62type streamExecutor struct {
63	upgrader  spdy.Upgrader
64	transport http.RoundTripper
65
66	method    string
67	url       *url.URL
68	protocols []string
69}
70
71// NewSPDYExecutor connects to the provided server and upgrades the connection to
72// multiplexed bidirectional streams.
73func NewSPDYExecutor(config *restclient.Config, method string, url *url.URL) (Executor, error) {
74	wrapper, upgradeRoundTripper, err := spdy.RoundTripperFor(config)
75	if err != nil {
76		return nil, err
77	}
78	return NewSPDYExecutorForTransports(wrapper, upgradeRoundTripper, method, url)
79}
80
81// NewSPDYExecutorForTransports connects to the provided server using the given transport,
82// upgrades the response using the given upgrader to multiplexed bidirectional streams.
83func NewSPDYExecutorForTransports(transport http.RoundTripper, upgrader spdy.Upgrader, method string, url *url.URL) (Executor, error) {
84	return NewSPDYExecutorForProtocols(
85		transport, upgrader, method, url,
86		remotecommand.StreamProtocolV4Name,
87		remotecommand.StreamProtocolV3Name,
88		remotecommand.StreamProtocolV2Name,
89		remotecommand.StreamProtocolV1Name,
90	)
91}
92
93// NewSPDYExecutorForProtocols connects to the provided server and upgrades the connection to
94// multiplexed bidirectional streams using only the provided protocols. Exposed for testing, most
95// callers should use NewSPDYExecutor or NewSPDYExecutorForTransports.
96func NewSPDYExecutorForProtocols(transport http.RoundTripper, upgrader spdy.Upgrader, method string, url *url.URL, protocols ...string) (Executor, error) {
97	return &streamExecutor{
98		upgrader:  upgrader,
99		transport: transport,
100		method:    method,
101		url:       url,
102		protocols: protocols,
103	}, nil
104}
105
106// Stream opens a protocol streamer to the server and streams until a client closes
107// the connection or the server disconnects.
108func (e *streamExecutor) Stream(options StreamOptions) error {
109	req, err := http.NewRequest(e.method, e.url.String(), nil)
110	if err != nil {
111		return fmt.Errorf("error creating request: %v", err)
112	}
113
114	conn, protocol, err := spdy.Negotiate(
115		e.upgrader,
116		&http.Client{Transport: e.transport},
117		req,
118		e.protocols...,
119	)
120	if err != nil {
121		return err
122	}
123	defer conn.Close()
124
125	var streamer streamProtocolHandler
126
127	switch protocol {
128	case remotecommand.StreamProtocolV4Name:
129		streamer = newStreamProtocolV4(options)
130	case remotecommand.StreamProtocolV3Name:
131		streamer = newStreamProtocolV3(options)
132	case remotecommand.StreamProtocolV2Name:
133		streamer = newStreamProtocolV2(options)
134	case "":
135		klog.V(4).Infof("The server did not negotiate a streaming protocol version. Falling back to %s", remotecommand.StreamProtocolV1Name)
136		fallthrough
137	case remotecommand.StreamProtocolV1Name:
138		streamer = newStreamProtocolV1(options)
139	}
140
141	return streamer.stream(conn)
142}
143