From 935b777e57901adab793454309fb9445a9330436 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 14:41:01 +0100 Subject: [PATCH] Factor out ssh service code --- cmd/ssh/main.go | 297 +++----------------------------------------- internal/ssh/ssh.go | 279 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 282 deletions(-) create mode 100644 internal/ssh/ssh.go diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go index 2741798..972bdaf 100644 --- a/cmd/ssh/main.go +++ b/cmd/ssh/main.go @@ -2,25 +2,18 @@ package main import ( "context" - "encoding/hex" - "errors" "flag" "fmt" - "net" "path" "os" "sync" - "golang.org/x/crypto/ssh" - "git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" - "git.defalsify.org/vise.git/resource" - "git.defalsify.org/vise.git/state" - "git.grassecon.net/urdt/ussd/internal/handlers" "git.grassecon.net/urdt/ussd/internal/storage" + "git.grassecon.net/urdt/ussd/internal/ssh" ) var ( @@ -30,274 +23,6 @@ var ( scriptDir = path.Join("services", "registration") ) -type auther struct { - Ctx context.Context - auth map[string]string -} - -func NewAuther(ctx context.Context) *auther { - return &auther{ - Ctx: ctx, - auth: make(map[string]string), - } -} - -func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { - keyStore.SetLanguage(nil) - keyStore.SetPrefix(storage.DATATYPE_CUSTOM) - k := append([]byte{0x01}, pubKey.Marshal()...) - v, err := keyStore.Get(a.Ctx, k) - if err != nil { - return nil, err - } - ka := hex.EncodeToString(conn.SessionID()) - va := string(v) - a.auth[ka] = va - fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, v) - return nil, nil -} - -func(a *auther) FromConn(c *ssh.ServerConn) (string, error) { - if c == nil { - return "", errors.New("nil server conn") - } - if c.Conn == nil { - return "", errors.New("nil underlying conn") - } - return a.Get(c.Conn.SessionID()) -} - - -func(a *auther) Get(k []byte) (string, error) { - ka := hex.EncodeToString(k) - v, ok := a.auth[ka] - if !ok { - return "", errors.New("not found") - } - return v, nil -} - -//func serve(ctx context.Context, sessionId string, ch ssh.NewChannel, mss *storage.MenuStorageService, lhs *handlers.LocalHandlerService) error { -func serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { - if ch == nil { - return errors.New("nil channel") - } - if ch.ChannelType() != "session" { - ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for") - return errors.New("not a session") - } - channel, requests, err := ch.Accept() - if err != nil { - panic(err) - } - defer channel.Close() - wg.Add(1) - go func(reqIn <-chan *ssh.Request) { - defer wg.Done() - for req := range reqIn { - req.Reply(req.Type == "shell", nil) - } - _ = requests - }(requests) - - cont, err := en.Exec(ctx, []byte{}) - if err != nil { - return fmt.Errorf("initial engine exec err: %v", err) - } - - var input [state.INPUT_LIMIT]byte - for cont { - c, err := en.Flush(ctx, channel) - if err != nil { - return fmt.Errorf("flush err: %v", err) - } - _, err = channel.Write([]byte{0x0a}) - if err != nil { - return fmt.Errorf("newline err: %v", err) - } - c, err = channel.Read(input[:]) - if err != nil { - return fmt.Errorf("read input fail: %v", err) - } - logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1]) - cont, err = en.Exec(ctx, input[:c-1]) - if err != nil { - return fmt.Errorf("engine exec err: %v", err) - } - logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en) - _ = c - } - c, err := en.Flush(ctx, channel) - if err != nil { - return fmt.Errorf("last flush err: %v", err) - } - _ = c - return nil -} - -type sshRunner struct { - Ctx context.Context - Cfg engine.Config - FlagFile string - DbDir string - ResourceDir string - Debug bool - KeyFile string - Host string - Port uint -} - -func(s *sshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { - ctx := s.Ctx - menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir) - - err := menuStorageService.EnsureDbDir() - if err != nil { - return nil, nil, err - } - - rs, err := menuStorageService.GetResource(ctx) - if err != nil { - return nil, nil, err - } - - pe, err := menuStorageService.GetPersister(ctx) - if err != nil { - return nil, nil, err - } - - userdatastore, err := menuStorageService.GetUserdataDb(ctx) - if err != nil { - return nil, nil, err - } - - dbResource, ok := rs.(*resource.DbResource) - if !ok { - return nil, nil, err - } - - lhs, err := handlers.NewLocalHandlerService(s.FlagFile, true, dbResource, s.Cfg, rs) - lhs.SetDataStore(&userdatastore) - lhs.SetPersister(pe) - lhs.Cfg.SessionId = sessionId - - if err != nil { - return nil, nil, err - } - - hl, err := lhs.GetHandler() - if err != nil { - return nil, nil, err - } - - en := lhs.GetEngine() - en = en.WithFirst(hl.Init) - if s.Debug { - en = en.WithDebug(nil) - } - // TODO: this is getting very hacky! - closer := func() { - err := menuStorageService.Close() - if err != nil { - logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err) - } - } - return en, closer, nil -} - -// adapted example from crypto/ssh package, NewServerConn doc -func(s *sshRunner) Run(ctx context.Context) { - running := true - - // TODO: waitgroup should probably not be global - defer wg.Wait() - - auth := NewAuther(ctx) - cfg := ssh.ServerConfig{ - PublicKeyCallback: auth.Check, - } - - privateBytes, err := os.ReadFile(s.KeyFile) - if err != nil { - logg.ErrorCtxf(ctx, "Failed to load private key", "err", err) - } - private, err := ssh.ParsePrivateKey(privateBytes) - if err != nil { - logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err) - } - cfg.AddHostKey(private) - - lst, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) - if err != nil { - panic(err) - } - - for running { - conn, err := lst.Accept() - if err != nil { - panic(err) - } - - - go func(conn net.Conn) { - defer conn.Close() - for true { - srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg) - if err != nil { - logg.InfoCtxf(ctx, "rejected client", "err", err) - return - } - logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn) - - wg.Add(1) - go func() { - ssh.DiscardRequests(rC) - wg.Done() - }() - - sessionId, err := auth.FromConn(srvConn) - if err != nil { - logg.ErrorCtxf(ctx, "Cannot find authentication") - return - } - en, closer, err := s.GetEngine(sessionId) - if err != nil { - logg.ErrorCtxf(ctx, "engine won't start", "err", err) - return - } - defer func() { - err := en.Finish() - if err != nil { - logg.ErrorCtxf(ctx, "engine won't stop", "err", err) - } - closer() - }() - for ch := range nC { - err = serve(ctx, sessionId, ch, en) - logg.ErrorCtxf(ctx, "ssh server finish", "err", err) - } - } - }(conn) - } -} - -// TODO: This is test code, move to external tool for adding and removing keys -func sshLoadKeys(ctx context.Context, dbDir string) error { - keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") - keyStore = storage.NewThreadGdbmDb() - err := keyStore.Connect(ctx, keyStoreFile) - if err != nil { - return err - } - pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCu5rYCxMBsVAL1TEkMQgmElAYEZj5zYDdyHjUxZ6qzHBOZD9GAzdxx9GyQDx2vdYm3329tLH/69ky1YA3nUz8SnJGBD6hC5XrqwN6zo9R9oOHAKTwiPGhey2NTVmheP+9XNHukBnOlkkWOQlpDDvMbWOztaZOWDaA8OIeP0t6qzFqLyelyg65lxzM3BKd7bCmmfzl/64BcP1MotAmB9DUxmY0Wb4Q2hYZfNYBx50Z4xthTgKV+Xoo8CbTduKotIz6hluQGvWdtxlCJQEiZ2f4RYY87JSA6/BAH2fhxuLHMXRpzocJNqARqCWpdcTGSg7bzxbKvTFH9OU4wZtr9ie40OR4zsc1lOBZL0rnp8GLkG8ZmeBQrgEDlmR9TTlz4okgtL+c5TCS37rjZYVjmtGwihws0EL9+wyv2dSQibirklC4wK5eWHKXl5vab19qzw/qRLdoRBK40DxbRKggxA7gqSsKrmrf+z7CuLIz/kxF+169FBLbh1MfBOGdx1awm6aU= lash@furioso")) - if err != nil { - return err - } - k := append([]byte{0x01}, pubKey.Marshal()...) - keyStore.SetPrefix(storage.DATATYPE_CUSTOM) - return keyStore.Put(ctx, k, []byte("+25113243546")) -} - func main() { var dbDir string var resourceDir string @@ -338,22 +63,30 @@ func main() { if engineDebug { cfg.EngineDebug = true } - - err = sshLoadKeys(ctx, dbDir) + + keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") + authKeyStore := storage.NewThreadGdbmDb() + err = authKeyStore.Connect(ctx, keyStoreFile) if err != nil { - fmt.Fprintf(os.Stderr, err.Error()) + fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) os.Exit(1) } + defer func() { + err := authKeyStore.Close() + if err != nil { + logg.ErrorCtxf(ctx, "keystore close error", "err", err) + } + }() - runner := &sshRunner{ + runner := &ssh.SshRunner{ Cfg: cfg, Debug: engineDebug, FlagFile: pfp, DbDir: dbDir, ResourceDir: resourceDir, - KeyFile: sshKeyFile, + SrvKeyFile: sshKeyFile, Host: host, Port: port, } - runner.Run(ctx) + runner.Run(ctx, authKeyStore) } diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go new file mode 100644 index 0000000..9cb00b3 --- /dev/null +++ b/internal/ssh/ssh.go @@ -0,0 +1,279 @@ +package ssh + +import ( + "context" + "encoding/hex" + "errors" + "fmt" + "net" + "os" + "sync" + + "golang.org/x/crypto/ssh" + + "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/logging" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/state" + + "git.grassecon.net/urdt/ussd/internal/handlers" + "git.grassecon.net/urdt/ussd/internal/storage" +) + +var ( + logg = logging.NewVanilla().WithDomain("ssh") +) + +type auther struct { + Ctx context.Context + keyStore db.Db + auth map[string]string +} + +func NewAuther(ctx context.Context, keyStore db.Db) *auther { + return &auther{ + Ctx: ctx, + keyStore: keyStore, + auth: make(map[string]string), + } +} + +func(a *auther) Check(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) { + a.keyStore.SetLanguage(nil) + a.keyStore.SetPrefix(storage.DATATYPE_CUSTOM) + k := append([]byte{0x01}, pubKey.Marshal()...) + v, err := a.keyStore.Get(a.Ctx, k) + if err != nil { + return nil, err + } + ka := hex.EncodeToString(conn.SessionID()) + va := string(v) + a.auth[ka] = va + fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, v) + return nil, nil +} + +func(a *auther) FromConn(c *ssh.ServerConn) (string, error) { + if c == nil { + return "", errors.New("nil server conn") + } + if c.Conn == nil { + return "", errors.New("nil underlying conn") + } + return a.Get(c.Conn.SessionID()) +} + + +func(a *auther) Get(k []byte) (string, error) { + ka := hex.EncodeToString(k) + v, ok := a.auth[ka] + if !ok { + return "", errors.New("not found") + } + return v, nil +} + +func(s *SshRunner) serve(ctx context.Context, sessionId string, ch ssh.NewChannel, en engine.Engine) error { + if ch == nil { + return errors.New("nil channel") + } + if ch.ChannelType() != "session" { + ch.Reject(ssh.UnknownChannelType, "that is not the channel you are looking for") + return errors.New("not a session") + } + channel, requests, err := ch.Accept() + if err != nil { + panic(err) + } + defer channel.Close() + s.wg.Add(1) + go func(reqIn <-chan *ssh.Request) { + defer s.wg.Done() + for req := range reqIn { + req.Reply(req.Type == "shell", nil) + } + _ = requests + }(requests) + + cont, err := en.Exec(ctx, []byte{}) + if err != nil { + return fmt.Errorf("initial engine exec err: %v", err) + } + + var input [state.INPUT_LIMIT]byte + for cont { + c, err := en.Flush(ctx, channel) + if err != nil { + return fmt.Errorf("flush err: %v", err) + } + _, err = channel.Write([]byte{0x0a}) + if err != nil { + return fmt.Errorf("newline err: %v", err) + } + c, err = channel.Read(input[:]) + if err != nil { + return fmt.Errorf("read input fail: %v", err) + } + logg.TraceCtxf(ctx, "input read", "c", c, "input", input[:c-1]) + cont, err = en.Exec(ctx, input[:c-1]) + if err != nil { + return fmt.Errorf("engine exec err: %v", err) + } + logg.TraceCtxf(ctx, "exec cont", "cont", cont, "en", en) + _ = c + } + c, err := en.Flush(ctx, channel) + if err != nil { + return fmt.Errorf("last flush err: %v", err) + } + _ = c + return nil +} + +type SshRunner struct { + Ctx context.Context + Cfg engine.Config + FlagFile string + DbDir string + ResourceDir string + Debug bool + SrvKeyFile string + Host string + Port uint + wg sync.WaitGroup +} + +func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { + ctx := s.Ctx + menuStorageService := storage.NewMenuStorageService(s.DbDir, s.ResourceDir) + + err := menuStorageService.EnsureDbDir() + if err != nil { + return nil, nil, err + } + + rs, err := menuStorageService.GetResource(ctx) + if err != nil { + return nil, nil, err + } + + pe, err := menuStorageService.GetPersister(ctx) + if err != nil { + return nil, nil, err + } + + userdatastore, err := menuStorageService.GetUserdataDb(ctx) + if err != nil { + return nil, nil, err + } + + dbResource, ok := rs.(*resource.DbResource) + if !ok { + return nil, nil, err + } + + lhs, err := handlers.NewLocalHandlerService(s.FlagFile, true, dbResource, s.Cfg, rs) + lhs.SetDataStore(&userdatastore) + lhs.SetPersister(pe) + lhs.Cfg.SessionId = sessionId + + if err != nil { + return nil, nil, err + } + + hl, err := lhs.GetHandler() + if err != nil { + return nil, nil, err + } + + en := lhs.GetEngine() + en = en.WithFirst(hl.Init) + if s.Debug { + en = en.WithDebug(nil) + } + // TODO: this is getting very hacky! + closer := func() { + err := menuStorageService.Close() + if err != nil { + logg.ErrorCtxf(ctx, "menu storage service cleanup fail", "err", err) + } + } + return en, closer, nil +} + +// adapted example from crypto/ssh package, NewServerConn doc +func(s *SshRunner) Run(ctx context.Context, keyStore db.Db) { + running := true + + // TODO: waitgroup should probably not be global + defer s.wg.Wait() + + auth := NewAuther(ctx, keyStore) + cfg := ssh.ServerConfig{ + PublicKeyCallback: auth.Check, + } + + privateBytes, err := os.ReadFile(s.SrvKeyFile) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to load private key", "err", err) + } + private, err := ssh.ParsePrivateKey(privateBytes) + if err != nil { + logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err) + } + cfg.AddHostKey(private) + + lst, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) + if err != nil { + panic(err) + } + + for running { + conn, err := lst.Accept() + if err != nil { + panic(err) + } + + + go func(conn net.Conn) { + defer conn.Close() + for true { + srvConn, nC, rC, err := ssh.NewServerConn(conn, &cfg) + if err != nil { + logg.InfoCtxf(ctx, "rejected client", "err", err) + return + } + logg.DebugCtxf(ctx, "ssh client connected", "conn", srvConn) + + s.wg.Add(1) + go func() { + ssh.DiscardRequests(rC) + s.wg.Done() + }() + + sessionId, err := auth.FromConn(srvConn) + if err != nil { + logg.ErrorCtxf(ctx, "Cannot find authentication") + return + } + en, closer, err := s.GetEngine(sessionId) + if err != nil { + logg.ErrorCtxf(ctx, "engine won't start", "err", err) + return + } + defer func() { + err := en.Finish() + if err != nil { + logg.ErrorCtxf(ctx, "engine won't stop", "err", err) + } + closer() + }() + for ch := range nC { + err = s.serve(ctx, sessionId, ch, en) + logg.ErrorCtxf(ctx, "ssh server finish", "err", err) + } + } + }(conn) + } +}