mirror of
https://github.com/gregtwallace/apc-p15-tool.git
synced 2025-01-22 08:14:08 +00:00
06c9263bc4
This was done for clearer separation of function. A subsequent update will (hopefully) make the SSL command more robust so it works for both NMC2 and NMC3. The method for sending shell commands was also updated to use an interactive shell instead. This allows capturing responses of the commands which will be needed to deduce if devices are NMC2 or NMC3.
134 lines
3.6 KiB
Go
134 lines
3.6 KiB
Go
package apcssh
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
)
|
|
|
|
const (
|
|
apcSSHVer = 1
|
|
|
|
sshTimeout = 90 * time.Second
|
|
)
|
|
|
|
// APC UPS won't except Go's SSH "Run()" command as the format isn't quite
|
|
// the same. Therefore, write a custom implementation instead of relying on
|
|
// something like github.com/bramvdbogaerde/go-scp
|
|
|
|
type Config struct {
|
|
Hostname string
|
|
Username string
|
|
Password string
|
|
ServerFingerprint string
|
|
InsecureCipher bool
|
|
}
|
|
|
|
// Client is an APC UPS SSH client
|
|
type Client struct {
|
|
hostname string
|
|
sshCfg *ssh.ClientConfig
|
|
}
|
|
|
|
// New creates a new SSH Client for the APC UPS.
|
|
func New(cfg *Config) (*Client, error) {
|
|
// make host key callback
|
|
hk := func(_hostname string, _remote net.Addr, key ssh.PublicKey) error {
|
|
// calculate server's key's SHA256
|
|
hasher := sha256.New()
|
|
_, err := hasher.Write(key.Marshal())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
actualHash := hasher.Sum(nil)
|
|
|
|
// log fingerprint for debugging
|
|
actualHashB64 := base64.RawStdEncoding.EncodeToString(actualHash)
|
|
actualHashHex := hex.EncodeToString(actualHash)
|
|
|
|
// check for fingerprint match (b64 or hex)
|
|
if actualHashB64 != cfg.ServerFingerprint && actualHashHex != cfg.ServerFingerprint {
|
|
log.Printf("apcssh: remote server key fingerprint (b64): %s", actualHashB64)
|
|
log.Printf("apcssh: remote server key fingerprint (hex): %s", actualHashHex)
|
|
|
|
return errors.New("apcssh: fingerprint didn't match")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// kex algos
|
|
// see defaults: https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.18.0:ssh/common.go;l=62
|
|
kexAlgos := []string{
|
|
"curve25519-sha256", "curve25519-sha256@libssh.org",
|
|
"ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521",
|
|
"diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1",
|
|
}
|
|
// extra for some apc ups
|
|
kexAlgos = append(kexAlgos, "diffie-hellman-group-exchange-sha256")
|
|
|
|
// ciphers
|
|
// see defaults: https://cs.opensource.google/go/x/crypto/+/master:ssh/common.go;l=37
|
|
ciphers := []string{
|
|
"aes128-gcm@openssh.com", "aes256-gcm@openssh.com",
|
|
"chacha20-poly1305@openssh.com",
|
|
"aes128-ctr", "aes192-ctr", "aes256-ctr",
|
|
}
|
|
|
|
// insecure cipher options?
|
|
if cfg.InsecureCipher {
|
|
log.Println("WARNING: insecure ciphers are enabled (--insecurecipher). SSH with an insecure cipher is NOT secure and should NOT be used.")
|
|
ciphers = append(ciphers, "aes128-cbc", "3des-cbc")
|
|
}
|
|
|
|
// install file on UPS
|
|
// ssh config
|
|
config := &ssh.ClientConfig{
|
|
User: cfg.Username,
|
|
Auth: []ssh.AuthMethod{
|
|
ssh.Password(cfg.Password),
|
|
},
|
|
// APC seems to require `Client Version` string to start with "SSH-2" and must be at least
|
|
// 13 characters long
|
|
// working examples from other clients:
|
|
// ClientVersion: "SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6",
|
|
// ClientVersion: "SSH-2.0-PuTTY_Release_0.80",
|
|
ClientVersion: fmt.Sprintf("SSH-2.0-apcssh_v%d %s-%s", apcSSHVer, runtime.GOOS, runtime.GOARCH),
|
|
Config: ssh.Config{
|
|
KeyExchanges: kexAlgos,
|
|
Ciphers: ciphers,
|
|
},
|
|
HostKeyCallback: hk,
|
|
|
|
// reasonable timeout for file copy
|
|
Timeout: sshTimeout,
|
|
}
|
|
|
|
// if hostname missing a port, add default
|
|
if !strings.Contains(cfg.Hostname, ":") {
|
|
cfg.Hostname = cfg.Hostname + ":22"
|
|
}
|
|
|
|
// connect to ups over SSH (to verify everything works)
|
|
sshClient, err := ssh.Dial("tcp", cfg.Hostname, config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_ = sshClient.Close()
|
|
|
|
// return Client (note: new ssh Dial will be done for each action as the UPS
|
|
// seems to not do well with more than one Session per Dial)
|
|
return &Client{
|
|
hostname: cfg.Hostname,
|
|
sshCfg: config,
|
|
}, nil
|
|
}
|