Added target and terminal support.
[src/xds/xds-server.git] / lib / xdsserver / terminal-ssh.go
diff --git a/lib/xdsserver/terminal-ssh.go b/lib/xdsserver/terminal-ssh.go
new file mode 100644 (file)
index 0000000..3f4a344
--- /dev/null
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2018 "IoT.bzh"
+ * Author Sebastien Douheret <sebastien@iot.bzh>
+ *
+ * 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/golib/eows"
+       "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/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.OutSplit = eows.SplitChar
+       t.sshWS.PtsMode = true
+
+       // Define callback for input (stdin)
+       t.sshWS.InputEvent = xsapiv1.TerminalInEvent
+       t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
+               t.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
+
+               // Handle Ctrl-D
+               if len(stdin) == 1 && stdin == "\x04" {
+                       // Close stdin
+                       errMsg := fmt.Errorf("close stdin: %v", stdin)
+                       return "", errMsg
+               }
+
+               return stdin, nil
+       }
+
+       // Define callback for output (stdout+stderr)
+       t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
+               // 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)
+
+               t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID)
+               if stdout != "" {
+                       t.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
+               }
+               if stderr != "" {
+                       t.Log.Debugf("STDERR <<%v>>", strings.Replace(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
+       if t.termCfg.Status != xsapiv1.StatusTermOpen {
+               return &t.termCfg, nil
+       }
+
+       err := t.sshWS.Signal("SIGTERM")
+
+       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
+       }
+
+       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)
+}