mirror of
https://github.com/gregtwallace/apc-p15-tool.git
synced 2025-01-22 16:14:09 +00:00
135 lines
3.6 KiB
Go
135 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
|
||
|
}
|