2
0
mirror of https://github.com/miguelmota/cointop synced 2024-11-10 13:10:26 +00:00
cointop/pkg/ssh/server.go

271 lines
6.2 KiB
Go
Raw Normal View History

2020-08-03 03:04:00 +00:00
//+build !windows
2020-07-26 20:40:59 +00:00
package ssh
import (
"context"
"crypto/sha256"
2020-07-26 20:40:59 +00:00
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
2021-04-06 08:11:34 +00:00
"strings"
2020-07-26 20:40:59 +00:00
"syscall"
"time"
"unsafe"
"github.com/creack/pty"
"github.com/gliderlabs/ssh"
2020-11-17 03:12:24 +00:00
"github.com/miguelmota/cointop/pkg/pathutil"
2020-07-26 20:40:59 +00:00
gossh "golang.org/x/crypto/ssh"
)
// DefaultHostKeyFile is default SSH key path
2020-08-03 02:52:24 +00:00
var DefaultHostKeyFile = "~/.ssh/id_rsa"
2021-04-06 08:11:34 +00:00
// UserConfigTypePublicKey is constant
var UserConfigTypePublicKey = "public-key"
// UserConfigTypeUsername is constant
var UserConfigTypeUsername = "username"
// UserConfigTypeNone is constant
var UserConfigTypeNone = "none"
var UserConfigTypes = []string{UserConfigTypePublicKey, UserConfigTypeUsername, UserConfigTypeNone}
// Config is config struct
2020-07-26 20:40:59 +00:00
type Config struct {
Port uint
Address string
IdleTimeout time.Duration
2021-02-23 06:20:40 +00:00
MaxTimeout time.Duration
2020-07-26 20:40:59 +00:00
ExecutableBinary string
2020-08-03 02:52:24 +00:00
HostKeyFile string
2021-03-17 06:18:44 +00:00
MaxSessions uint
2021-04-06 08:11:34 +00:00
UserConfigType string
2021-09-08 05:26:37 +00:00
ColorsDir string
2020-07-26 20:40:59 +00:00
}
// Server is server struct
2020-07-26 20:40:59 +00:00
type Server struct {
port uint
address string
idleTimeout time.Duration
2021-02-23 06:20:40 +00:00
maxTimeout time.Duration
2020-07-26 20:40:59 +00:00
executableBinary string
sshServer *ssh.Server
2020-08-03 02:52:24 +00:00
hostKeyFile string
2021-03-17 06:18:44 +00:00
maxSessions uint
sessionCount uint
2021-04-06 08:11:34 +00:00
userConfigType string
2021-09-08 05:26:37 +00:00
colorsDir string
2020-07-26 20:40:59 +00:00
}
// NewServer returns a new server instance
2020-07-26 20:40:59 +00:00
func NewServer(config *Config) *Server {
2020-08-03 02:52:24 +00:00
hostKeyFile := DefaultHostKeyFile
if config.HostKeyFile != "" {
hostKeyFile = config.HostKeyFile
}
2021-04-06 08:11:34 +00:00
userConfigType := config.UserConfigType
if userConfigType == "" {
userConfigType = UserConfigTypePublicKey
}
validateUserConfigType(userConfigType)
2020-08-03 02:52:24 +00:00
hostKeyFile = pathutil.NormalizePath(hostKeyFile)
2021-09-08 05:26:37 +00:00
colorsDir := pathutil.NormalizePath("~/.config/cointop/colors")
if config.ColorsDir != "" {
colorsDir = pathutil.NormalizePath(config.ColorsDir)
}
2020-07-26 20:40:59 +00:00
return &Server{
port: config.Port,
address: config.Address,
idleTimeout: config.IdleTimeout,
2021-02-23 06:20:40 +00:00
maxTimeout: config.MaxTimeout,
2020-07-26 20:40:59 +00:00
executableBinary: config.ExecutableBinary,
2020-08-03 02:52:24 +00:00
hostKeyFile: hostKeyFile,
2021-03-17 06:18:44 +00:00
maxSessions: config.MaxSessions,
2021-04-06 08:11:34 +00:00
userConfigType: userConfigType,
2021-09-08 05:26:37 +00:00
colorsDir: colorsDir,
2020-07-26 20:40:59 +00:00
}
}
// ListenAndServe starts the server
2020-07-26 20:40:59 +00:00
func (s *Server) ListenAndServe() error {
s.sshServer = &ssh.Server{
Addr: fmt.Sprintf("%s:%v", s.address, s.port),
IdleTimeout: s.idleTimeout,
2021-02-23 06:20:40 +00:00
MaxTimeout: s.maxTimeout,
2020-07-26 20:40:59 +00:00
Handler: func(sshSession ssh.Session) {
2021-03-17 06:18:44 +00:00
if s.maxSessions > 0 {
s.sessionCount++
defer func() {
s.sessionCount--
}()
if s.sessionCount > s.maxSessions {
io.WriteString(sshSession, "Error: Maximum sessions reached. Must wait until session slot is available.")
sshSession.Exit(1)
return
}
}
2020-08-22 05:27:40 +00:00
cmdUserArgs := sshSession.Command()
2020-07-26 20:40:59 +00:00
ptyReq, winCh, isPty := sshSession.Pty()
if !isPty {
io.WriteString(sshSession, "Error: Non-interactive terminals are not supported")
sshSession.Exit(1)
return
}
configDir := ""
2021-04-06 08:11:34 +00:00
configDirKey := ""
switch s.userConfigType {
case UserConfigTypePublicKey:
pubKey := sshSession.PublicKey()
if pubKey != nil {
pubBytes := pubKey.Marshal()
if len(pubBytes) > 0 {
hash := sha256.Sum256(pubBytes)
configDirKey = fmt.Sprintf("%x", hash)
}
}
2021-04-06 08:11:34 +00:00
case UserConfigTypeUsername:
user := sshSession.User()
if user != "" {
configDirKey = user
}
}
if configDirKey != "" {
configDir = fmt.Sprintf("/tmp/cointop_config/%s", configDirKey)
err := os.MkdirAll(configDir, 0700)
if err != nil {
fmt.Println(err)
return
}
}
if configDir == "" {
tempDir, err := createTempDir()
if err != nil {
fmt.Println(err)
return
}
configDir = tempDir
defer os.RemoveAll(configDir)
2020-07-26 20:40:59 +00:00
}
configPath := fmt.Sprintf("%s/config", configDir)
2020-08-01 07:37:49 +00:00
2020-07-26 20:40:59 +00:00
cmdCtx, cancelCmd := context.WithCancel(sshSession.Context())
defer cancelCmd()
2020-08-21 23:19:08 +00:00
flags := []string{
"--reset",
"--silent",
"--cache-dir",
configDir,
2020-08-21 23:19:08 +00:00
"--config",
configPath,
2020-08-22 05:27:40 +00:00
"--colors-dir",
2021-09-08 05:26:37 +00:00
s.colorsDir,
2020-08-21 23:19:08 +00:00
}
2021-04-25 20:51:20 +00:00
if len(cmdUserArgs) > 0 {
if cmdUserArgs[0] == "cointop" {
cmdUserArgs = cmdUserArgs[1:]
2020-08-21 23:19:08 +00:00
}
2021-04-25 20:51:20 +00:00
}
if len(cmdUserArgs) > 0 {
if cmdUserArgs[0] == "holdings" {
flags = []string{
"--config",
configPath,
}
}
}
2020-08-21 23:19:08 +00:00
2021-04-25 20:51:20 +00:00
for _, arg := range cmdUserArgs {
2020-08-21 23:19:08 +00:00
flags = append(flags, arg)
}
cmd := exec.CommandContext(cmdCtx, s.executableBinary, flags...)
2020-07-26 20:40:59 +00:00
cmd.Env = append(sshSession.Environ(), fmt.Sprintf("TERM=%s", ptyReq.Term))
f, err := pty.Start(cmd)
if err != nil {
io.WriteString(sshSession, err.Error())
}
defer f.Close()
go func() {
for win := range winCh {
setWinsize(f, win.Width, win.Height)
}
}()
go func() {
io.Copy(f, sshSession)
}()
io.Copy(sshSession, f)
f.Close()
cmd.Wait()
},
PtyCallback: func(ctx ssh.Context, pty ssh.Pty) bool {
return true
},
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
return true
},
PasswordHandler: func(ctx ssh.Context, password string) bool {
return true
},
KeyboardInteractiveHandler: func(ctx ssh.Context, challenger gossh.KeyboardInteractiveChallenge) bool {
return true
},
}
2020-08-03 02:52:24 +00:00
if _, err := os.Stat(s.hostKeyFile); os.IsNotExist(err) {
2020-07-26 20:40:59 +00:00
return errors.New("SSH key is required to start server")
}
2020-08-03 02:52:24 +00:00
err := s.sshServer.SetOption(ssh.HostKeyFile(s.hostKeyFile))
2020-07-26 20:40:59 +00:00
if err != nil {
return err
}
return s.sshServer.ListenAndServe()
}
// Shutdown shuts down the server
2020-07-26 20:40:59 +00:00
func (s *Server) Shutdown() {
s.sshServer.Close()
}
// setWinsize sets the PTY window size
2020-07-26 20:40:59 +00:00
func setWinsize(f *os.File, w, h int) {
syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(syscall.TIOCSWINSZ),
uintptr(unsafe.Pointer(&struct{ h, w, x, y uint16 }{uint16(h), uint16(w), 0, 0})))
}
// createTempDir creates a temporary directory
2020-08-01 07:37:49 +00:00
func createTempDir() (string, error) {
return ioutil.TempDir("", "")
2020-07-26 20:40:59 +00:00
}
2021-04-06 08:11:34 +00:00
func validateUserConfigType(userConfigType string) {
for _, validType := range UserConfigTypes {
if validType == userConfigType {
return
}
}
panic(fmt.Errorf("invalid user config type. Acceptable values are: %s", strings.Join(UserConfigTypes, ",")))
}