/* * Copyright (C) 2018 "IoT.bzh" * Author Sebastien Douheret * * 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 xdsserver import ( "fmt" "strings" "time" "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/eows" "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/lib/xsapiv1" socketio "github.com/googollee/go-socket.io" uuid "github.com/satori/go.uuid" ) // ITARGET interface implementation for standard targets // TermSSH . type TermSSH struct { *Context termCfg xsapiv1.TerminalConfig targetID string sshWS *eows.ExecOverWS } // NewTermSSH Create a new instance of TermSSH func NewTermSSH(ctx *Context, cfg xsapiv1.TerminalConfig, targetID string) *TermSSH { // Allocate and set default settings t := TermSSH{ Context: ctx, termCfg: xsapiv1.TerminalConfig{ ID: cfg.ID, Name: "ssh", Type: xsapiv1.TypeTermSSH, Status: xsapiv1.StatusTermClose, User: "", Options: []string{""}, Cols: 80, Rows: 24, }, targetID: targetID, } t.UpdateConfig(cfg) return &t } // NewUID Get a UUID func (t *TermSSH) _NewUID(suffix string) string { uuid := uuid.NewV1().String() if len(suffix) > 0 { uuid += "_" + suffix } return uuid } // GetConfig Get public part of terminal config func (t *TermSSH) GetConfig() xsapiv1.TerminalConfig { return t.termCfg } // UpdateConfig Update terminal config func (t *TermSSH) UpdateConfig(newCfg xsapiv1.TerminalConfig) *xsapiv1.TerminalConfig { if t.termCfg.ID == "" { if newCfg.ID != "" { t.termCfg.ID = newCfg.ID } else { t.termCfg.ID = t._NewUID("") } } if newCfg.Name != "" { t.termCfg.Name = newCfg.Name } if newCfg.User != "" { t.termCfg.User = newCfg.User } if len(newCfg.Options) > 0 { t.termCfg.Options = newCfg.Options } // Adjust terminal size t.Resize(newCfg.Cols, newCfg.Rows) return &t.termCfg } // Open a new terminal - execute ssh command and bind stdin/stdout to WebSocket func (t *TermSSH) Open(sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) { // Get target info to retrieve IP tgt := t.targets.Get(t.targetID) if tgt == nil { return nil, fmt.Errorf("Cannot retrieve target definition") } tgtCfg := (*tgt).GetConfig() // Sanity check if tgtCfg.IP == "" { return nil, fmt.Errorf("null target IP") } userStr := "" if t.termCfg.User != "" { userStr = t.termCfg.User + "@" } // Compute ssh command cmd := "ssh" cmdID := "ssh_term_" + t.termCfg.ID args := t.termCfg.Options args = append(args, userStr+tgtCfg.IP) t.sshWS = eows.New(cmd, args, sock, sessID, cmdID) t.sshWS.Log = t.Log t.sshWS.PtyMode = true // Define callback for input (stdin) t.sshWS.InputEvent = xsapiv1.TerminalInEvent t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin []byte) ([]byte, error) { if t.LogLevelSilly { t.Log.Debugf("STDIN <<%v>> %s", stdin, string(stdin)) } return stdin, nil } // Define callback for output (stdout+stderr) t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr []byte) { // IO socket can be nil when disconnected so := t.sessions.IOSocketGet(e.Sid) if so == nil { t.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.TerminalOutEvent, e.Sid, e.CmdID) return } // Retrieve project ID and RootPath data := e.UserData termID := (*data)["ID"].(string) if t.LogLevelSilly { t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID) if len(stdout) > 0 { t.Log.Debugf("STDOUT <<%v>>", strings.Replace(string(stdout), "\n", "\\n", -1)) } if len(stderr) > 0 { t.Log.Debugf("STDERR <<%v>>", strings.Replace(string(stderr), "\n", "\\n", -1)) } } // FIXME replace by .BroadcastTo a room err := (*so).Emit(xsapiv1.TerminalOutEvent, xsapiv1.TerminalOutMsg{ TermID: termID, Timestamp: time.Now().String(), Stdout: stdout, Stderr: stderr, }) if err != nil { t.Log.Errorf("WS Emit : %v", err) } } // Define callback for output t.sshWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) { t.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err) // IO socket can be nil when disconnected so := t.sessions.IOSocketGet(e.Sid) if so == nil { t.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.TerminalExitEvent, e.CmdID) return } // Retrieve project ID and RootPath data := e.UserData termID := (*data)["ID"].(string) // FIXME replace by .BroadcastTo a room errSoEmit := (*so).Emit(xsapiv1.TerminalExitEvent, xsapiv1.TerminalExitMsg{ TermID: termID, Timestamp: time.Now().String(), Code: code, Error: err, }) if errSoEmit != nil { t.Log.Errorf("WS Emit : %v", errSoEmit) } t.termCfg.Status = xsapiv1.StatusTermClose t.sshWS = nil } // data (used within callbacks) data := make(map[string]interface{}) data["ID"] = t.termCfg.ID t.sshWS.UserData = &data // Start ssh command t.Log.Infof("Execute [Cmd ID %s]: %v %v", t.sshWS.CmdID, t.sshWS.Cmd, t.sshWS.Args) if err := t.sshWS.Start(); err != nil { return &t.termCfg, err } t.termCfg.Status = xsapiv1.StatusTermOpen return &t.termCfg, nil } // Close a terminal func (t *TermSSH) Close() (*xsapiv1.TerminalConfig, error) { // nothing to do when not open or closing if !(t.termCfg.Status == xsapiv1.StatusTermOpen || t.termCfg.Status == xsapiv1.StatusTermClosing) { return &t.termCfg, nil } err := t.sshWS.Signal("SIGTERM") t.termCfg.Status = xsapiv1.StatusTermClosing return &t.termCfg, err } // Resize a terminal func (t *TermSSH) Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error) { if t.sshWS == nil { return &t.termCfg, fmt.Errorf("ssh session not initialized") } if cols > 0 { t.termCfg.Cols = cols } if rows > 0 { t.termCfg.Rows = rows } t.LogSillyf("Terminal resize id=%v, cols=%v, rows=%v", t.termCfg.ID, cols, rows) err := t.sshWS.TerminalSetSize(t.termCfg.Rows, t.termCfg.Cols) if err != nil { t.Log.Errorf("Error ssh TerminalSetSize: %v", err) } return &t.termCfg, err } // Signal Send a signal to a terminal func (t *TermSSH) Signal(sigName string) error { return t.sshWS.Signal(sigName) }