/
cmd.go
363 lines (316 loc) · 8.59 KB
/
cmd.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
package main
import (
// "bufio"
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"os/exec"
"strings"
"time"
)
// @author Robin Verlangen
type Cmd struct {
Command string // Commands to execute
Pending bool // Did we dispatch it to the client?
Id string // Unique ID for this command
ClientId string // Client ID on which the command is executed
TemplateId string // Reference to the template id
ConsensusRequestId string // Reference to the request id
Signature string // makes this only valid from the server to the client based on the preshared token and this is a signature with the command and id
Timeout int // in seconds
State string // Textual representation of the current state, e.g. finished, failed, etc.
RequestUserId string // User ID of the user that initiated this command
Created int64 // Unix timestamp created
ExecutionIterationId int // In which iteration the command was started
BufOutput []string // Standard output
BufOutputErr []string // Error output
}
// Sign the command on the server
func (c *Cmd) Sign(client *RegisteredClient) {
c.Signature = c.ComputeHmac(client.AuthToken)
}
// Set local state
func (c *Cmd) SetState(state string) {
// Old state for change detection
oldState := c.State
// Update
c.State = state
// Debug logging
if conf.Debug {
log.Printf("Cmd %s went from state %s to %s", c.Id, oldState, c.State)
}
// Run validation
if oldState == "finished_execution" && c.State == "flushed_logs" {
c._validate()
} else if oldState == "failed_execution" && c.State == "flushed_logs" {
c.State = "failed"
}
}
// Validate the execution of a command, only on the server
func (c *Cmd) _validate() {
// Only on the server
if !conf.ServerEnabled {
return
}
// Get template
template := server.templateStore.Get(c.TemplateId)
if template == nil {
log.Printf("Unable to find template %s for validation of cmd %s", c.TemplateId, c.Id)
return
}
// Iterate and run on templates
var failedValidation = false
for _, v := range template.ValidationRules {
// Select stream
var stream []string
if v.OutputStream == 1 {
stream = c.BufOutput
} else {
stream = c.BufOutputErr
}
// Match on line
var matched bool = false
for _, line := range stream {
if strings.Contains(line, v.Text) {
matched = true
break
}
}
// Did we match?
if v.MustContain == true && matched == false {
// Should BE there, but is NOT
c.SetState("failed_validation")
failedValidation = true
break
} else if v.MustContain == false && matched == true {
// Should NOT be there, but IS
c.SetState("failed_validation")
failedValidation = true
break
}
}
// Done and passed validation
if failedValidation == false {
if conf.Debug {
log.Printf("Validation passed for %s", c.Id)
}
c.SetState("finished")
// Start next iteration
ece := server.executionCoordinator.Get(c.ConsensusRequestId)
if ece != nil {
go ece.Next()
}
}
}
// Notify state to server
func (c *Cmd) NotifyServer(state string) {
// Update local client state
c.SetState(state)
// Update server state, only if this has a signature, else it is local
if len(c.Signature) > 0 {
client._req("PUT", fmt.Sprintf("client/%s/cmd/%s/state?state=%s", url.QueryEscape(client.Id), url.QueryEscape(c.Id), url.QueryEscape(state)), nil)
}
}
// Should we flush the local buffer? After X milliseconds or Y lines
func (c *Cmd) _checkFlushLogs() {
// At least 10 lines
if len(c.BufOutput) > 10 || len(c.BufOutputErr) > 10 {
c._flushLogs()
}
}
// Write logs to server
func (c *Cmd) _flushLogs() {
// Only if this has a signature, else it is local
if len(c.Signature) < 1 {
return
}
// To JSON
m := make(map[string][]string)
m["output"] = c.BufOutput
m["error"] = c.BufOutputErr
bytes, je := json.Marshal(m)
if je != nil {
log.Printf("Failed to convert logs to JSON: %s", je)
return
}
// Post to server
uri := fmt.Sprintf("client/%s/cmd/%s/logs", url.QueryEscape(client.Id), url.QueryEscape(c.Id))
b, e := client._req("PUT", uri, bytes)
if e != nil || len(b) < 1 {
log.Printf("Failed log write: %s", e)
}
// Clear buffers
c.BufOutput = make([]string, 0)
c.BufOutputErr = make([]string, 0)
}
// Log output
func (c *Cmd) LogOutput(line string) {
// No lock, only one routine can access this
// Append
c.BufOutput = append(c.BufOutput, line)
// Check to flush?
c._checkFlushLogs()
}
// Log error
func (c *Cmd) LogError(line string) {
// No lock, only one routine can access this
// Append
c.BufOutputErr = append(c.BufOutputErr, line)
// Check to flush?
c._checkFlushLogs()
}
// Sign the command
func (c *Cmd) ComputeHmac(token string) string {
bytes, be := base64.URLEncoding.DecodeString(token)
if be != nil {
return ""
}
mac := hmac.New(sha256.New, bytes)
mac.Write([]byte(c.Command))
mac.Write([]byte(c.Id))
sum := mac.Sum(nil)
return base64.URLEncoding.EncodeToString(sum)
}
// Execute command on the client
func (c *Cmd) Execute(client *Client) {
log.Printf("Executing %s: %s", c.Id, c.Command)
// Validate HMAC
c.NotifyServer("validating")
if client != nil {
// Compute mac
expectedMac := c.ComputeHmac(client.AuthToken)
// Valid?
if expectedMac != c.Signature || len(c.Signature) < 1 {
// No, let's abort
// Notify server
c.NotifyServer("invalid_signature")
// Log
log.Printf("ERROR! Invalid command signature, communication between server and client might be tampered with")
// Re-authenticate with server in order to establish a new token
client.AuthServer()
// Abort execution
return
}
} else {
log.Printf("Executing insecure command, unable to validate HMAC of %s", c.Id)
}
// Start
c.NotifyServer("starting")
// File contents
var fileBytes bytes.Buffer
fileBytes.WriteString("#!/bin/bash\n")
fileBytes.WriteString(c.Command)
// Write tmp file
tmpFileName := fmt.Sprintf("/tmp/indispenso_%s", c.Id)
ioutil.WriteFile(tmpFileName, fileBytes.Bytes(), 0644)
// Remove file once done
defer os.Remove(tmpFileName)
// Run file
cmd := exec.Command("bash", tmpFileName)
var out bytes.Buffer
var outerr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &outerr
// Consume streams
// go func() {
// p, pe := cmd.StdoutPipe()
// if pe != nil {
// log.Printf("Pipe error: %s", pe)
// return
// }
// scanner := bufio.NewScanner(p)
// for scanner.Scan() {
// txt := scanner.Text()
// c.LogOutput(txt)
// if conf.Debug {
// log.Println(scanner.Text())
// }
// }
// if err := scanner.Err(); err != nil {
// fmt.Fprintln(os.Stderr, "reading standard input:", err)
// }
// }()
// go func() {
// p, pe := cmd.StderrPipe()
// if pe != nil {
// log.Printf("Pipe error: %s", pe)
// return
// }
// scanner := bufio.NewScanner(p)
// for scanner.Scan() {
// txt := scanner.Text()
// c.LogError(txt)
// if conf.Debug {
// log.Println(scanner.Text())
// }
// }
// if err := scanner.Err(); err != nil {
// fmt.Fprintln(os.Stderr, "reading standard input:", err)
// }
// }()
// Start
err := cmd.Start()
if err != nil {
c.NotifyServer("failed_execution")
log.Printf("Failed to start command: %s", err)
return
}
c.NotifyServer("started_execution")
// Timeout mechanism
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
select {
case <-time.After(time.Duration(c.Timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill %s: %s", c.Id, err)
return
}
<-done // allow goroutine to exit
c.NotifyServer("killed_execution")
log.Printf("Process %s killed", c.Id)
case err := <-done:
if err != nil {
c.NotifyServer("failed_execution")
c.LogError(fmt.Sprintf("%v", err))
log.Printf("Process %s done with error = %v", c.Id, err)
} else {
c.NotifyServer("finished_execution")
log.Printf("Finished %s", c.Id)
}
}
// Logs
for _, line := range strings.Split(out.String(), "\n") {
c.LogOutput(line)
}
for _, line := range strings.Split(outerr.String(), "\n") {
c.LogError(line)
}
// Final flush
c._flushLogs()
c.NotifyServer("flushed_logs")
}
func newCmd(command string, timeout int) *Cmd {
// Default timeout if not valid
if timeout < 1 {
timeout = DEFAULT_COMMAND_TIMEOUT
}
// Create instance
return &Cmd{
Id: uuidStr(),
Command: command,
Pending: true,
Timeout: timeout,
State: "pending",
Created: time.Now().Unix(),
BufOutput: make([]string, 0),
BufOutputErr: make([]string, 0),
}
}