Use go module as dependency tool instead of glide
[src/xds/xds-server.git] / lib / xdsserver / terminal-ssh.go
1 /*
2  * Copyright (C) 2018 "IoT.bzh"
3  * Author Sebastien Douheret <sebastien@iot.bzh>
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at
8  *
9  *   http://www.apache.org/licenses/LICENSE-2.0
10  *
11  * Unless required by applicable law or agreed to in writing, software
12  * distributed under the License is distributed on an "AS IS" BASIS,
13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14  * See the License for the specific language governing permissions and
15  * limitations under the License.
16  */
17
18 package xdsserver
19
20 import (
21         "fmt"
22         "strings"
23         "time"
24
25         "gerrit.automotivelinux.org/gerrit/src/xds/xds-common.git/eows"
26         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server.git/lib/xsapiv1"
27         socketio "github.com/googollee/go-socket.io"
28         uuid "github.com/satori/go.uuid"
29 )
30
31 // ITARGET interface implementation for standard targets
32
33 // TermSSH .
34 type TermSSH struct {
35         *Context
36         termCfg  xsapiv1.TerminalConfig
37         targetID string
38         sshWS    *eows.ExecOverWS
39 }
40
41 // NewTermSSH Create a new instance of TermSSH
42 func NewTermSSH(ctx *Context, cfg xsapiv1.TerminalConfig, targetID string) *TermSSH {
43
44         // Allocate and set default settings
45         t := TermSSH{
46                 Context: ctx,
47                 termCfg: xsapiv1.TerminalConfig{
48                         ID:      cfg.ID,
49                         Name:    "ssh",
50                         Type:    xsapiv1.TypeTermSSH,
51                         Status:  xsapiv1.StatusTermClose,
52                         User:    "",
53                         Options: []string{""},
54                         Cols:    80,
55                         Rows:    24,
56                 },
57                 targetID: targetID,
58         }
59
60         t.UpdateConfig(cfg)
61         return &t
62 }
63
64 // NewUID Get a UUID
65 func (t *TermSSH) _NewUID(suffix string) string {
66         uuid := uuid.NewV1().String()
67         if len(suffix) > 0 {
68                 uuid += "_" + suffix
69         }
70         return uuid
71 }
72
73 // GetConfig Get public part of terminal config
74 func (t *TermSSH) GetConfig() xsapiv1.TerminalConfig {
75         return t.termCfg
76 }
77
78 // UpdateConfig Update terminal config
79 func (t *TermSSH) UpdateConfig(newCfg xsapiv1.TerminalConfig) *xsapiv1.TerminalConfig {
80
81         if t.termCfg.ID == "" {
82                 if newCfg.ID != "" {
83                         t.termCfg.ID = newCfg.ID
84                 } else {
85                         t.termCfg.ID = t._NewUID("")
86                 }
87         }
88         if newCfg.Name != "" {
89                 t.termCfg.Name = newCfg.Name
90         }
91         if newCfg.User != "" {
92                 t.termCfg.User = newCfg.User
93         }
94         if len(newCfg.Options) > 0 {
95                 t.termCfg.Options = newCfg.Options
96         }
97
98         // Adjust terminal size
99         t.Resize(newCfg.Cols, newCfg.Rows)
100
101         return &t.termCfg
102 }
103
104 // Open a new terminal - execute ssh command and bind stdin/stdout to WebSocket
105 func (t *TermSSH) Open(sock *socketio.Socket, sessID string) (*xsapiv1.TerminalConfig, error) {
106
107         // Get target info to retrieve IP
108         tgt := t.targets.Get(t.targetID)
109         if tgt == nil {
110                 return nil, fmt.Errorf("Cannot retrieve target definition")
111         }
112         tgtCfg := (*tgt).GetConfig()
113
114         // Sanity check
115         if tgtCfg.IP == "" {
116                 return nil, fmt.Errorf("null target IP")
117         }
118         userStr := ""
119         if t.termCfg.User != "" {
120                 userStr = t.termCfg.User + "@"
121         }
122
123         // Compute ssh command
124         cmd := "ssh"
125         cmdID := "ssh_term_" + t.termCfg.ID
126         args := t.termCfg.Options
127         args = append(args, userStr+tgtCfg.IP)
128
129         t.sshWS = eows.New(cmd, args, sock, sessID, cmdID)
130         t.sshWS.Log = t.Log
131         t.sshWS.PtyMode = true
132
133         // Define callback for input (stdin)
134         t.sshWS.InputEvent = xsapiv1.TerminalInEvent
135         t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin []byte) ([]byte, error) {
136                 if t.LogLevelSilly {
137                         t.Log.Debugf("STDIN <<%v>> %s", stdin, string(stdin))
138                 }
139                 return stdin, nil
140         }
141
142         // Define callback for output (stdout+stderr)
143         t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr []byte) {
144                 // IO socket can be nil when disconnected
145                 so := t.sessions.IOSocketGet(e.Sid)
146                 if so == nil {
147                         t.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.TerminalOutEvent, e.Sid, e.CmdID)
148                         return
149                 }
150
151                 // Retrieve project ID and RootPath
152                 data := e.UserData
153                 termID := (*data)["ID"].(string)
154
155                 if t.LogLevelSilly {
156                         t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID)
157                         if len(stdout) > 0 {
158                                 t.Log.Debugf("STDOUT <<%v>>", strings.Replace(string(stdout), "\n", "\\n", -1))
159                         }
160                         if len(stderr) > 0 {
161                                 t.Log.Debugf("STDERR <<%v>>", strings.Replace(string(stderr), "\n", "\\n", -1))
162                         }
163                 }
164
165                 // FIXME replace by .BroadcastTo a room
166                 err := (*so).Emit(xsapiv1.TerminalOutEvent, xsapiv1.TerminalOutMsg{
167                         TermID:    termID,
168                         Timestamp: time.Now().String(),
169                         Stdout:    stdout,
170                         Stderr:    stderr,
171                 })
172                 if err != nil {
173                         t.Log.Errorf("WS Emit : %v", err)
174                 }
175         }
176
177         // Define callback for output
178         t.sshWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
179                 t.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
180
181                 // IO socket can be nil when disconnected
182                 so := t.sessions.IOSocketGet(e.Sid)
183                 if so == nil {
184                         t.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.TerminalExitEvent, e.CmdID)
185                         return
186                 }
187
188                 // Retrieve project ID and RootPath
189                 data := e.UserData
190                 termID := (*data)["ID"].(string)
191
192                 // FIXME replace by .BroadcastTo a room
193                 errSoEmit := (*so).Emit(xsapiv1.TerminalExitEvent, xsapiv1.TerminalExitMsg{
194                         TermID:    termID,
195                         Timestamp: time.Now().String(),
196                         Code:      code,
197                         Error:     err,
198                 })
199                 if errSoEmit != nil {
200                         t.Log.Errorf("WS Emit : %v", errSoEmit)
201                 }
202
203                 t.termCfg.Status = xsapiv1.StatusTermClose
204                 t.sshWS = nil
205         }
206
207         // data (used within callbacks)
208         data := make(map[string]interface{})
209         data["ID"] = t.termCfg.ID
210         t.sshWS.UserData = &data
211
212         // Start ssh command
213         t.Log.Infof("Execute [Cmd ID %s]: %v %v", t.sshWS.CmdID, t.sshWS.Cmd, t.sshWS.Args)
214
215         if err := t.sshWS.Start(); err != nil {
216                 return &t.termCfg, err
217         }
218
219         t.termCfg.Status = xsapiv1.StatusTermOpen
220
221         return &t.termCfg, nil
222 }
223
224 // Close a terminal
225 func (t *TermSSH) Close() (*xsapiv1.TerminalConfig, error) {
226         // nothing to do when not open or closing
227         if !(t.termCfg.Status == xsapiv1.StatusTermOpen || t.termCfg.Status == xsapiv1.StatusTermClosing) {
228                 return &t.termCfg, nil
229         }
230
231         err := t.sshWS.Signal("SIGTERM")
232
233         t.termCfg.Status = xsapiv1.StatusTermClosing
234
235         return &t.termCfg, err
236 }
237
238 // Resize a terminal
239 func (t *TermSSH) Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
240         if t.sshWS == nil {
241                 return &t.termCfg, fmt.Errorf("ssh session not initialized")
242         }
243
244         if cols > 0 {
245                 t.termCfg.Cols = cols
246         }
247         if rows > 0 {
248                 t.termCfg.Rows = rows
249         }
250
251         t.LogSillyf("Terminal resize id=%v, cols=%v, rows=%v", t.termCfg.ID, cols, rows)
252
253         err := t.sshWS.TerminalSetSize(t.termCfg.Rows, t.termCfg.Cols)
254         if err != nil {
255                 t.Log.Errorf("Error ssh TerminalSetSize: %v", err)
256         }
257
258         return &t.termCfg, err
259 }
260
261 // Signal Send a signal to a terminal
262 func (t *TermSSH) Signal(sigName string) error {
263         return t.sshWS.Signal(sigName)
264 }