mirror of
https://github.com/gregtwallace/apc-p15-tool.git
synced 2025-01-22 08:14:08 +00:00
703c26bd27
It was possible for scanner.Scan() to block indefinitely if the UPS never returned the expected prompt regex pattern. This could occur with a UPS using a prompt format I'm not aware of, or if the UPS responds in a non-standard way. This change ensures that Scan() is aborted after a fixed amount of blocking time and the shell cmd function accordingly returns an error. Some error messages, comments, and var names are also updated for clarity.
139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package apcssh
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
// Abort shell connection if UPS doesn't send a recognizable response within
|
|
// the specified timeouts; Cmd timeout is very long as it is unlikely to be
|
|
// needed but still exists to avoid an indefinite hang in the unlikely event
|
|
// something does go wrong at that part of the app
|
|
const (
|
|
shellTimeoutLogin = 20 * time.Second
|
|
shellTimeoutCmd = 5 * time.Minute
|
|
)
|
|
|
|
// upsCmdResult is a structure that holds all of a shell commands results
|
|
type upsCmdResult struct {
|
|
command string
|
|
code string
|
|
codeText string
|
|
resultText string
|
|
}
|
|
|
|
// cmd creates an interactive shell and executes the specified command
|
|
func (cli *Client) cmd(command string) (*upsCmdResult, error) {
|
|
// connect
|
|
sshClient, err := ssh.Dial("tcp", cli.hostname, cli.sshCfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to dial client (%w)", err)
|
|
}
|
|
defer sshClient.Close()
|
|
|
|
session, err := sshClient.NewSession()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create session (%w)", err)
|
|
}
|
|
defer session.Close()
|
|
|
|
// pipes to send shell command to; and to receive repsonse
|
|
sshInput, err := session.StdinPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make shell input pipe (%w)", err)
|
|
}
|
|
sshOutput, err := session.StdoutPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to make shell output pipe (%w)", err)
|
|
}
|
|
|
|
// make scanner to read shell output continuously
|
|
scanner := bufio.NewScanner(sshOutput)
|
|
scanner.Split(scanAPCShell)
|
|
|
|
// start interactive shell
|
|
if err := session.Shell(); err != nil {
|
|
return nil, fmt.Errorf("failed to start shell (%w)", err)
|
|
}
|
|
|
|
// use a timer to close the session early in case Scan() hangs (which can
|
|
// happen if the UPS provides output this app does not understand)
|
|
cancelAbort := make(chan struct{})
|
|
defer close(cancelAbort)
|
|
go func() {
|
|
select {
|
|
case <-time.After(shellTimeoutLogin):
|
|
_ = session.Close()
|
|
|
|
case <-cancelAbort:
|
|
// aborted cancel (i.e., succesful Scan())
|
|
}
|
|
}()
|
|
|
|
// check shell response after connect
|
|
scannedOk := scanner.Scan()
|
|
// if failed to scan (e.g., timer closed the session after timeout)
|
|
if !scannedOk {
|
|
return nil, errors.New("shell did not return parsable login response")
|
|
}
|
|
// success; cancel abort timer
|
|
cancelAbort <- struct{}{}
|
|
// discard the initial shell response (login message(s) / initial shell prompt)
|
|
_ = scanner.Bytes()
|
|
|
|
// send command
|
|
_, err = fmt.Fprint(sshInput, command+"\n")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to send shell command (%w)", err)
|
|
}
|
|
|
|
// use a timer to close the session early in case Scan() hangs (which can
|
|
// happen if the UPS provides output this app does not understand);
|
|
// since initial login message Scan() was okay, it is relatively unlikely this
|
|
// will hang
|
|
go func() {
|
|
select {
|
|
case <-time.After(shellTimeoutCmd):
|
|
_ = session.Close()
|
|
|
|
case <-cancelAbort:
|
|
// aborted cancel (i.e., succesful Scan())
|
|
}
|
|
}()
|
|
|
|
// check shell response to command
|
|
scannedOk = scanner.Scan()
|
|
// if failed to scan (e.g., timer closed the session after timeout)
|
|
if !scannedOk {
|
|
return nil, fmt.Errorf("shell did not return parsable response to cmd '%s'", command)
|
|
}
|
|
// success; cancel abort timer
|
|
cancelAbort <- struct{}{}
|
|
|
|
// parse the UPS response into result struct and return
|
|
upsRawResponse := string(scanner.Bytes())
|
|
result := &upsCmdResult{}
|
|
|
|
cmdIndx := strings.Index(upsRawResponse, "\n")
|
|
result.command = upsRawResponse[:cmdIndx-1]
|
|
upsRawResponse = upsRawResponse[cmdIndx+1:]
|
|
|
|
codeIndx := strings.Index(upsRawResponse, ": ")
|
|
result.code = upsRawResponse[:codeIndx]
|
|
upsRawResponse = upsRawResponse[codeIndx+2:]
|
|
|
|
codeTxtIndx := strings.Index(upsRawResponse, "\n")
|
|
result.codeText = upsRawResponse[:codeTxtIndx-1]
|
|
|
|
// avoid out of bounds if no result text
|
|
if codeTxtIndx+1 <= len(upsRawResponse)-2 {
|
|
result.resultText = upsRawResponse[codeTxtIndx+1 : len(upsRawResponse)-2]
|
|
}
|
|
|
|
return result, nil
|
|
}
|