Added target and terminal support.
[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/golib/eows"
26         "gerrit.automotivelinux.org/gerrit/src/xds/xds-server/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.OutSplit = eows.SplitChar
132         t.sshWS.PtsMode = true
133
134         // Define callback for input (stdin)
135         t.sshWS.InputEvent = xsapiv1.TerminalInEvent
136         t.sshWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
137                 t.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
138
139                 // Handle Ctrl-D
140                 if len(stdin) == 1 && stdin == "\x04" {
141                         // Close stdin
142                         errMsg := fmt.Errorf("close stdin: %v", stdin)
143                         return "", errMsg
144                 }
145
146                 return stdin, nil
147         }
148
149         // Define callback for output (stdout+stderr)
150         t.sshWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
151                 // IO socket can be nil when disconnected
152                 so := t.sessions.IOSocketGet(e.Sid)
153                 if so == nil {
154                         t.Log.Infof("%s not emitted: WS closed (sid:%s, CmdID:%s)", xsapiv1.TerminalOutEvent, e.Sid, e.CmdID)
155                         return
156                 }
157
158                 // Retrieve project ID and RootPath
159                 data := e.UserData
160                 termID := (*data)["ID"].(string)
161
162                 t.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - termID:%s", xsapiv1.TerminalOutEvent, e.Sid[4:], e.CmdID, termID)
163                 if stdout != "" {
164                         t.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
165                 }
166                 if stderr != "" {
167                         t.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
168                 }
169
170                 // FIXME replace by .BroadcastTo a room
171                 err := (*so).Emit(xsapiv1.TerminalOutEvent, xsapiv1.TerminalOutMsg{
172                         TermID:    termID,
173                         Timestamp: time.Now().String(),
174                         Stdout:    stdout,
175                         Stderr:    stderr,
176                 })
177                 if err != nil {
178                         t.Log.Errorf("WS Emit : %v", err)
179                 }
180         }
181
182         // Define callback for output
183         t.sshWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
184                 t.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
185
186                 // IO socket can be nil when disconnected
187                 so := t.sessions.IOSocketGet(e.Sid)
188                 if so == nil {
189                         t.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.TerminalExitEvent, e.CmdID)
190                         return
191                 }
192
193                 // Retrieve project ID and RootPath
194                 data := e.UserData
195                 termID := (*data)["ID"].(string)
196
197                 // FIXME replace by .BroadcastTo a room
198                 errSoEmit := (*so).Emit(xsapiv1.TerminalExitEvent, xsapiv1.TerminalExitMsg{
199                         TermID:    termID,
200                         Timestamp: time.Now().String(),
201                         Code:      code,
202                         Error:     err,
203                 })
204                 if errSoEmit != nil {
205                         t.Log.Errorf("WS Emit : %v", errSoEmit)
206                 }
207
208                 t.termCfg.Status = xsapiv1.StatusTermClose
209                 t.sshWS = nil
210         }
211
212         // data (used within callbacks)
213         data := make(map[string]interface{})
214         data["ID"] = t.termCfg.ID
215         t.sshWS.UserData = &data
216
217         // Start ssh command
218         t.Log.Infof("Execute [Cmd ID %s]: %v %v", t.sshWS.CmdID, t.sshWS.Cmd, t.sshWS.Args)
219
220         if err := t.sshWS.Start(); err != nil {
221                 return &t.termCfg, err
222         }
223
224         t.termCfg.Status = xsapiv1.StatusTermOpen
225
226         return &t.termCfg, nil
227 }
228
229 // Close a terminal
230 func (t *TermSSH) Close() (*xsapiv1.TerminalConfig, error) {
231         // nothing to do when not open
232         if t.termCfg.Status != xsapiv1.StatusTermOpen {
233                 return &t.termCfg, nil
234         }
235
236         err := t.sshWS.Signal("SIGTERM")
237
238         return &t.termCfg, err
239 }
240
241 // Resize a terminal
242 func (t *TermSSH) Resize(cols, rows uint16) (*xsapiv1.TerminalConfig, error) {
243         if t.sshWS == nil {
244                 return &t.termCfg, fmt.Errorf("ssh session not initialized")
245         }
246
247         if cols > 0 {
248                 t.termCfg.Cols = cols
249         }
250         if rows > 0 {
251                 t.termCfg.Rows = rows
252         }
253
254         err := t.sshWS.TerminalSetSize(t.termCfg.Rows, t.termCfg.Cols)
255         if err != nil {
256                 t.Log.Errorf("Error ssh TerminalSetSize: %v", err)
257         }
258
259         return &t.termCfg, err
260 }
261
262 // Signal Send a signal to a terminal
263 func (t *TermSSH) Signal(sigName string) error {
264         return t.sshWS.Signal(sigName)
265 }