// Copyright 2018 The Kubesphere 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. // // the code is mainly from: // https://github.com/kubernetes/dashboard/blob/master/src/app/backend/handler/terminal.go // thanks to the related developer package terminal import ( "encoding/json" "fmt" "github.com/gorilla/websocket" "io" "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" "k8s.io/client-go/tools/remotecommand" "k8s.io/klog" "time" ) const ( // Time allowed to write a message to the peer. writeWait = 10 * time.Second ) // PtyHandler is what remotecommand expects from a pty type PtyHandler interface { io.Reader io.Writer remotecommand.TerminalSizeQueue } // TerminalSession implements PtyHandler (using a SockJS connection) type TerminalSession struct { conn *websocket.Conn sizeChan chan remotecommand.TerminalSize } // TerminalMessage is the messaging protocol between ShellController and TerminalSession. // // OP DIRECTION FIELD(S) USED DESCRIPTION // --------------------------------------------------------------------- // stdin fe->be Data Keystrokes/paste buffer // resize fe->be Rows, Cols New terminal size // stdout be->fe Data Output from the process // toast be->fe Data OOB message to be shown to the user type TerminalMessage struct { Op, Data string Rows, Cols uint16 } // TerminalSize handles pty->process resize events // Called in a loop from remotecommand as long as the process is running func (t TerminalSession) Next() *remotecommand.TerminalSize { select { case size := <-t.sizeChan: return &size } } // Read handles pty->process messages (stdin, resize) // Called in a loop from remotecommand as long as the process is running func (t TerminalSession) Read(p []byte) (int, error) { var msg TerminalMessage err := t.conn.ReadJSON(&msg) if err != nil { return 0, err } switch msg.Op { case "stdin": return copy(p, msg.Data), nil case "resize": t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows} return 0, nil default: return 0, fmt.Errorf("unknown message type '%s'", msg.Op) } } // Write handles process->pty stdout // Called from remotecommand whenever there is any output func (t TerminalSession) Write(p []byte) (int, error) { msg, err := json.Marshal(TerminalMessage{ Op: "stdout", Data: string(p), }) if err != nil { return 0, err } t.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err = t.conn.WriteMessage(websocket.TextMessage, msg); err != nil { return 0, err } return len(p), nil } // Toast can be used to send the user any OOB messages // hterm puts these in the center of the terminal func (t TerminalSession) Toast(p string) error { msg, err := json.Marshal(TerminalMessage{ Op: "toast", Data: p, }) if err != nil { return err } t.conn.SetWriteDeadline(time.Now().Add(writeWait)) if err = t.conn.WriteMessage(websocket.TextMessage, msg); err != nil { return err } return nil } // Close shuts down the SockJS connection and sends the status code and reason to the client // Can happen if the process exits or if there is an error starting up the process // For now the status code is unused and reason is shown to the user (unless "") func (t TerminalSession) Close(status uint32, reason string) { klog.Warning(status, reason) t.conn.Close() } type Interface interface { HandleSession(shell, namespace, podName, containerName string, conn *websocket.Conn) } type terminaler struct { client kubernetes.Interface config *rest.Config } func NewTerminaler(client kubernetes.Interface, config *rest.Config) Interface { return &terminaler{client: client, config: config} } // startProcess is called by handleAttach // Executed cmd in the container specified in request and connects it up with the ptyHandler (a session) func (t *terminaler) startProcess(namespace, podName, containerName string, cmd []string, ptyHandler PtyHandler) error { req := t.client.CoreV1().RESTClient().Post(). Resource("pods"). Name(podName). Namespace(namespace). SubResource("exec") req.VersionedParams(&v1.PodExecOptions{ Container: containerName, Command: cmd, Stdin: true, Stdout: true, Stderr: true, TTY: true, }, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(t.config, "POST", req.URL()) if err != nil { return err } err = exec.Stream(remotecommand.StreamOptions{ Stdin: ptyHandler, Stdout: ptyHandler, Stderr: ptyHandler, TerminalSizeQueue: ptyHandler, Tty: true, }) if err != nil { return err } return nil } // isValidShell checks if the shell is an allowed one func isValidShell(validShells []string, shell string) bool { for _, validShell := range validShells { if validShell == shell { return true } } return false } func (t *terminaler) HandleSession(shell, namespace, podName, containerName string, conn *websocket.Conn) { var err error validShells := []string{"sh", "bash"} session := &TerminalSession{conn: conn, sizeChan: make(chan remotecommand.TerminalSize)} if isValidShell(validShells, shell) { cmd := []string{shell} err = t.startProcess(namespace, podName, containerName, cmd, session) } else { // No shell given or it was not valid: try some shells until one succeeds or all fail // FIXME: if the first shell fails then the first keyboard event is lost for _, testShell := range validShells { cmd := []string{testShell} if err = t.startProcess(namespace, podName, containerName, cmd, session); err == nil { break } } } if err != nil { session.Close(2, err.Error()) return } session.Close(1, "Process exited") }