From 19372c17f401702fcd299e3f08c58738fdea22a4 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 03:06:24 +0100 Subject: [PATCH 01/34] Reinstate ssh executable --- cmd/ssh/main.go | 359 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 cmd/ssh/main.go diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go new file mode 100644 index 0000000..2741798 --- /dev/null +++ b/cmd/ssh/main.go @@ -0,0 +1,359 @@ +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" +) + +var ( + wg sync.WaitGroup + keyStore db.Db + logg = logging.NewVanilla() + 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 + var size uint + var engineDebug bool + var stateDebug bool + var host string + var port uint + flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") + flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") + flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") + flag.UintVar(&size, "s", 160, "max size of output") + flag.StringVar(&host, "h", "127.0.0.1", "http host") + flag.UintVar(&port, "p", 7122, "http port") + flag.Parse() + + sshKeyFile := flag.Arg(0) + _, err := os.Stat(sshKeyFile) + if err != nil { + fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) + os.Exit(1) + } + + logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) + + ctx := context.Background() + pfp := path.Join(scriptDir, "pp.csv") + + cfg := engine.Config{ + Root: "root", + OutputSize: uint32(size), + FlagCount: uint32(16), + } + if stateDebug { + cfg.StateDebug = true + } + if engineDebug { + cfg.EngineDebug = true + } + + err = sshLoadKeys(ctx, dbDir) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + runner := &sshRunner{ + Cfg: cfg, + Debug: engineDebug, + FlagFile: pfp, + DbDir: dbDir, + ResourceDir: resourceDir, + KeyFile: sshKeyFile, + Host: host, + Port: port, + } + runner.Run(ctx) +} From 935b777e57901adab793454309fb9445a9330436 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 14:41:01 +0100 Subject: [PATCH 02/34] 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) + } +} From ada1f26b680331d9c6fb1e3ff5ea0b37a9df35ee Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 15:41:55 +0100 Subject: [PATCH 03/34] Add keystore class, separate keystore tool executable --- cmd/ssh/main.go | 37 ++++++++++++++++++----- cmd/ssh/sshkey/main.go | 44 +++++++++++++++++++++++++++ internal/ssh/keystore.go | 64 ++++++++++++++++++++++++++++++++++++++++ internal/ssh/ssh.go | 29 +++++++++--------- 4 files changed, 153 insertions(+), 21 deletions(-) create mode 100644 cmd/ssh/sshkey/main.go create mode 100644 internal/ssh/keystore.go diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go index 972bdaf..0227616 100644 --- a/cmd/ssh/main.go +++ b/cmd/ssh/main.go @@ -6,13 +6,14 @@ import ( "fmt" "path" "os" + "os/signal" "sync" + "syscall" "git.defalsify.org/vise.git/db" "git.defalsify.org/vise.git/engine" "git.defalsify.org/vise.git/logging" - "git.grassecon.net/urdt/ussd/internal/storage" "git.grassecon.net/urdt/ussd/internal/ssh" ) @@ -47,9 +48,15 @@ func main() { os.Exit(1) } + ctx := context.Background() + logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING") + logg.WarnCtxf(ctx, "!!!!! =======================") + logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!") + logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") + logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") + logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) - ctx := context.Background() pfp := path.Join(scriptDir, "pp.csv") cfg := engine.Config{ @@ -64,20 +71,24 @@ func main() { cfg.EngineDebug = true } - keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") - authKeyStore := storage.NewThreadGdbmDb() - err = authKeyStore.Connect(ctx, keyStoreFile) + authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir) if err != nil { fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) os.Exit(1) } - defer func() { - err := authKeyStore.Close() + defer func () { + logg.TraceCtxf(ctx, "shutdown auth key store reached") + err = authKeyStore.Close() if err != nil { logg.ErrorCtxf(ctx, "keystore close error", "err", err) } }() + cint := make(chan os.Signal) + cterm := make(chan os.Signal) + signal.Notify(cint, os.Interrupt, syscall.SIGINT) + signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) + runner := &ssh.SshRunner{ Cfg: cfg, Debug: engineDebug, @@ -88,5 +99,17 @@ func main() { Host: host, Port: port, } + go func() { + select { + case _ = <-cint: + case _ = <-cterm: + } + logg.TraceCtxf(ctx, "shutdown runner reached") + err := runner.Stop() + if err != nil { + logg.ErrorCtxf(ctx, "runner stop error", "err", err) + } + + }() runner.Run(ctx, authKeyStore) } diff --git a/cmd/ssh/sshkey/main.go b/cmd/ssh/sshkey/main.go new file mode 100644 index 0000000..87b89a3 --- /dev/null +++ b/cmd/ssh/sshkey/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "context" + "flag" + "fmt" + "os" + + "git.grassecon.net/urdt/ussd/internal/ssh" +) + +func main() { + var dbDir string + var sessionId string + flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") + flag.StringVar(&sessionId, "i", "", "session id") + flag.Parse() + + if sessionId == "" { + fmt.Fprintf(os.Stderr, "empty session id\n") + os.Exit(1) + } + + ctx := context.Background() + + sshKeyFile := flag.Arg(0) + if sshKeyFile == "" { + fmt.Fprintf(os.Stderr, "missing key file argument\n") + os.Exit(1) + } + + store, err := ssh.NewSshKeyStore(ctx, dbDir) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } + defer store.Close() + + err = store.AddFromFile(ctx, sshKeyFile, sessionId) + if err != nil { + fmt.Fprintf(os.Stderr, "%v\n", err) + os.Exit(1) + } +} diff --git a/internal/ssh/keystore.go b/internal/ssh/keystore.go new file mode 100644 index 0000000..fed7233 --- /dev/null +++ b/internal/ssh/keystore.go @@ -0,0 +1,64 @@ +package ssh + +import ( + "context" + "fmt" + "os" + "path" + + "golang.org/x/crypto/ssh" + + "git.defalsify.org/vise.git/db" + + "git.grassecon.net/urdt/ussd/internal/storage" +) + +type SshKeyStore struct { + store db.Db +} + +func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) { + keyStore := &SshKeyStore{} + keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") + keyStore.store = storage.NewThreadGdbmDb() + err := keyStore.store.Connect(ctx, keyStoreFile) + if err != nil { + return nil, err + } + return keyStore, nil +} + +func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error { + _, err := os.Stat(fp) + if err != nil { + return fmt.Errorf("cannot open ssh server public key file: %v\n", err) + } + + publicBytes, err := os.ReadFile(fp) + if err != nil { + return fmt.Errorf("Failed to load public key: %v", err) + } + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes) + if err != nil { + return fmt.Errorf("Failed to parse public key: %v", err) + } + k := append([]byte{0x01}, pubKey.Marshal()...) + s.store.SetPrefix(storage.DATATYPE_CUSTOM) + logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes)) + return s.store.Put(ctx, k, []byte(sessionId)) +} + +func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) { + s.store.SetLanguage(nil) + s.store.SetPrefix(storage.DATATYPE_CUSTOM) + k := append([]byte{0x01}, pubKey.Marshal()...) + v, err := s.store.Get(ctx, k) + if err != nil { + return "", err + } + return string(v), nil +} + +func(s *SshKeyStore) Close() error { + return s.store.Close() +} diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go index 9cb00b3..31cbc3e 100644 --- a/internal/ssh/ssh.go +++ b/internal/ssh/ssh.go @@ -11,7 +11,6 @@ import ( "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" @@ -27,11 +26,11 @@ var ( type auther struct { Ctx context.Context - keyStore db.Db + keyStore *SshKeyStore auth map[string]string } -func NewAuther(ctx context.Context, keyStore db.Db) *auther { +func NewAuther(ctx context.Context, keyStore *SshKeyStore) *auther { return &auther{ Ctx: ctx, keyStore: keyStore, @@ -40,17 +39,13 @@ func NewAuther(ctx context.Context, keyStore db.Db) *auther { } 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) + va, err := a.keyStore.Get(a.Ctx, pubKey) 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) + fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va) return nil, nil } @@ -142,6 +137,11 @@ type SshRunner struct { Host string Port uint wg sync.WaitGroup + lst net.Listener +} + +func(s *SshRunner) Stop() error { + return s.lst.Close() } func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { @@ -203,7 +203,7 @@ func(s *SshRunner) GetEngine(sessionId string) (engine.Engine, func(), error) { } // adapted example from crypto/ssh package, NewServerConn doc -func(s *SshRunner) Run(ctx context.Context, keyStore db.Db) { +func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { running := true // TODO: waitgroup should probably not be global @@ -224,18 +224,19 @@ func(s *SshRunner) Run(ctx context.Context, keyStore db.Db) { } cfg.AddHostKey(private) - lst, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) + s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) if err != nil { panic(err) } for running { - conn, err := lst.Accept() + conn, err := s.lst.Accept() if err != nil { - panic(err) + logg.ErrorCtxf(ctx, "ssh accept error", "err", err) + running = false + continue } - go func(conn net.Conn) { defer conn.Close() for true { From e4c3e9f015bc908ccb2c91eb63a170cc30ec76c3 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 16:09:57 +0100 Subject: [PATCH 04/34] Add ssh instructions, log host key --- cmd/ssh/README.md | 34 ++++++++++++++++++++++++++++++++++ internal/ssh/ssh.go | 4 ++++ 2 files changed, 38 insertions(+) create mode 100644 cmd/ssh/README.md diff --git a/cmd/ssh/README.md b/cmd/ssh/README.md new file mode 100644 index 0000000..071557b --- /dev/null +++ b/cmd/ssh/README.md @@ -0,0 +1,34 @@ +# URDT-USSD SSH server + +An SSH server entry point for the vise engine. + + +## Adding public keys for access + +Map your (client) public key to a session identifier (e.g. phone number) + +``` +go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i [--dbdir ] +``` + + +## Create a private key for the server + +``` +ssh-keygen -N "" -f +``` + + +## Run the server + + +``` +go run -v -tags logtrace ./cmd/ssh/main.go -h -p [--dbdir ] +``` + + +## Connect to the server + +``` +ssh -T -p +``` diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go index 31cbc3e..394f55f 100644 --- a/internal/ssh/ssh.go +++ b/internal/ssh/ssh.go @@ -3,6 +3,7 @@ package ssh import ( "context" "encoding/hex" + "encoding/base64" "errors" "fmt" "net" @@ -222,6 +223,9 @@ func(s *SshRunner) Run(ctx context.Context, keyStore *SshKeyStore) { if err != nil { logg.ErrorCtxf(ctx, "Failed to parse private key", "err", err) } + srvPub := private.PublicKey() + srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal()) + logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr) cfg.AddHostKey(private) s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) From 3f3dbf414c5063bba8395999fed13ab1a2d75798 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 16:13:30 +0100 Subject: [PATCH 05/34] Clarify keyfile use in readme --- cmd/ssh/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/ssh/README.md b/cmd/ssh/README.md index 071557b..3cffae1 100644 --- a/cmd/ssh/README.md +++ b/cmd/ssh/README.md @@ -8,14 +8,14 @@ An SSH server entry point for the vise engine. Map your (client) public key to a session identifier (e.g. phone number) ``` -go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i [--dbdir ] +go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i [--dbdir ] ``` ## Create a private key for the server ``` -ssh-keygen -N "" -f +ssh-keygen -N "" -f ``` @@ -23,12 +23,12 @@ ssh-keygen -N "" -f ``` -go run -v -tags logtrace ./cmd/ssh/main.go -h -p [--dbdir ] +go run -v -tags logtrace ./cmd/ssh/main.go -h -p [--dbdir ] ``` ## Connect to the server ``` -ssh -T -p +ssh -T -p -i ``` From c2b68231f5652f45752db96ec7008f8086a4cae6 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 16:15:40 +0100 Subject: [PATCH 06/34] Update gitignore --- .gitignore | 2 ++ cmd/ssh/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 54ad3ff..ddccccf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ go.work* **/*/*.bin **/*/.state/ cmd/.state/ +id_* +*.gdbm diff --git a/cmd/ssh/README.md b/cmd/ssh/README.md index 3cffae1..ff325d7 100644 --- a/cmd/ssh/README.md +++ b/cmd/ssh/README.md @@ -30,5 +30,5 @@ go run -v -tags logtrace ./cmd/ssh/main.go -h -p [--dbdir ## Connect to the server ``` -ssh -T -p -i +ssh [-v] -T -p -i ``` From ddc8f6dad12ea9f3e28d22de66fc27d9f966a848 Mon Sep 17 00:00:00 2001 From: lash Date: Sun, 22 Sep 2024 16:24:01 +0100 Subject: [PATCH 07/34] Update module dep --- go.mod | 5 ++++- go.sum | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/go.mod b/go.mod index 7cceaa6..486f8ee 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,15 @@ module git.grassecon.net/urdt/ussd go 1.22.6 require ( - git.defalsify.org/vise.git v0.1.0-rc.3.0.20240920144308-b2d2c5f18f38 + git.defalsify.org/vise.git v0.1.0-rc.3.0.20240922152136-7ea16f9137b4 github.com/alecthomas/assert/v2 v2.2.2 github.com/peteole/testdata-loader v0.3.0 + golang.org/x/crypto v0.17.0 gopkg.in/leonelquinteros/gotext.v1 v1.3.1 ) +require golang.org/x/sys v0.15.0 // indirect + require ( github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect diff --git a/go.sum b/go.sum index 5d235cc..8dfead8 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,5 @@ -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f h1:CuJvG3NyMoRtHUim4aZdrfjjJBg2AId7z0yp7Q97bRM= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911231817-0d23e0dbb57f/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240914163514-577f56f43bea h1:6ZYT+dIjd/f5vn9y5AJDZ7SQQckA6w5ZfUoKygyI11o= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240914163514-577f56f43bea/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240920144308-b2d2c5f18f38 h1:4aAZijIcq33ixnZ+U48ckDIkwSfZL3St/CqoXZcC5K8= -git.defalsify.org/vise.git v0.1.0-rc.3.0.20240920144308-b2d2c5f18f38/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240922152136-7ea16f9137b4 h1:IMVUK9OkZ/QtYZPHgTZ+XUs5VQ4eIewIaTyVSCF/nAY= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240922152136-7ea16f9137b4/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= @@ -32,6 +28,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/leonelquinteros/gotext.v1 v1.3.1 h1:8d9/fdTG0kn/B7NNGV1BsEyvektXFAbkMsTZS2sFSCc= From 5c85ecffd1b7ffa10649ea86646421be6ad0c503 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 09:00:13 +0300 Subject: [PATCH 08/34] remove unused code --- internal/handlers/handlerservice.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 93c5a10..ad591bf 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -88,7 +88,6 @@ func (localHandlerService *LocalHandlerService) GetHandler() (*ussd.Handlers, er localHandlerService.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) localHandlerService.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) localHandlerService.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) - localHandlerService.DbRs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) localHandlerService.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) localHandlerService.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) localHandlerService.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) From eff2cbde8b864ddbd7c69231ecfb200118ee11ed Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 09:00:31 +0300 Subject: [PATCH 09/34] setup check for unused code --- internal/utils/isocode.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 internal/utils/isocode.go diff --git a/internal/utils/isocode.go b/internal/utils/isocode.go new file mode 100644 index 0000000..34365b0 --- /dev/null +++ b/internal/utils/isocode.go @@ -0,0 +1,15 @@ +package utils + + + + +var isoCodes = map[string]bool{ + "eng": true, // English + "swa": true, // Swahili + +} + +func IsValidISO639(code string) bool { + return isoCodes[code] +} + From bbfe46f162e64aa22f1caf78f3bbd75c16107881 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 09:01:19 +0300 Subject: [PATCH 10/34] use current rendering symbol for case selection --- internal/handlers/ussd/menuhandler.go | 62 ++++++--------------------- 1 file changed, 12 insertions(+), 50 deletions(-) diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index ff8d8bd..8ffecc3 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -117,17 +117,14 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource func (h *Handlers) SetLanguage(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sym, _ = h.st.Where() + symbol, _ := h.st.Where() + code := strings.Split(symbol, "_")[1] - switch sym { - case "set_default": - res.FlagSet = append(res.FlagSet, state.FLAG_LANG) - res.Content = "eng" - case "set_swa": - res.FlagSet = append(res.FlagSet, state.FLAG_LANG) - res.Content = "swa" - default: + if !utils.IsValidISO639(code) { + return res, nil } + res.FlagSet = append(res.FlagSet, state.FLAG_LANG) + res.Content = code languageSetFlag, err := h.flagManager.GetFlag("flag_language_set") if err != nil { @@ -279,32 +276,6 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt return res, nil } -// SetResetSingleEdit sets and resets flags to allow gradual editing of profile information. -func (h *Handlers) SetResetSingleEdit(ctx context.Context, sym string, input []byte) (resource.Result, error) { - var res resource.Result - - menuOption := string(input) - - flag_allow_update, _ := h.flagManager.GetFlag("flag_allow_update") - flag_single_edit, _ := h.flagManager.GetFlag("flag_single_edit") - - switch menuOption { - case "2": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - case "3": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - case "4": - res.FlagReset = append(res.FlagReset, flag_allow_update) - res.FlagSet = append(res.FlagSet, flag_single_edit) - default: - res.FlagReset = append(res.FlagReset, flag_single_edit) - } - - return res, nil -} - // VerifyPin checks whether the confirmation PIN is similar to the account PIN // If similar, it sets the USERFLAG_PIN_SET flag allowing the user // to access the main menu @@ -435,6 +406,7 @@ func (h *Handlers) SaveLocation(ctx context.Context, sym string, input []byte) ( // SaveGender updates the gender in the gdbm with the provided input. func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (resource.Result, error) { + symbol, _ := h.st.Where() var res resource.Result var err error sessionId, ok := ctx.Value("SessionId").(string) @@ -442,21 +414,11 @@ func (h *Handlers) SaveGender(ctx context.Context, sym string, input []byte) (re return res, fmt.Errorf("missing session") } - if len(input) > 0 { - gender := string(input) - switch gender { - case "1": - gender = "Male" - case "2": - gender = "Female" - case "3": - gender = "Unspecified" - } - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) - if err != nil { - return res, nil - } + gender := strings.Split(symbol, "_")[1] + store := h.userdataStore + err = store.WriteEntry(ctx, sessionId, utils.DATA_GENDER, []byte(gender)) + if err != nil { + return res, nil } return res, nil From 1bd96d0689f442110f46e43604e58b8838f19390 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 09:02:04 +0300 Subject: [PATCH 11/34] setup dedicated nodes for case selection --- services/registration/edit_profile.vis | 2 -- services/registration/select_gender.vis | 8 +++++--- services/registration/select_language.vis | 2 +- services/registration/{set_default.vis => set_eng.vis} | 0 services/registration/set_female.vis | 4 ++++ services/registration/set_male.vis | 4 ++++ services/registration/set_unspecified.vis | 4 ++++ 7 files changed, 18 insertions(+), 6 deletions(-) rename services/registration/{set_default.vis => set_eng.vis} (100%) create mode 100644 services/registration/set_female.vis create mode 100644 services/registration/set_male.vis create mode 100644 services/registration/set_unspecified.vis diff --git a/services/registration/edit_profile.vis b/services/registration/edit_profile.vis index 6c1986f..9d45ec9 100644 --- a/services/registration/edit_profile.vis +++ b/services/registration/edit_profile.vis @@ -11,8 +11,6 @@ MOUT view 7 MOUT back 0 HALT INCMP my_account 0 -LOAD set_reset_single_edit 0 -RELOAD set_reset_single_edit INCMP enter_name 1 INCMP enter_familyname 2 INCMP select_gender 3 diff --git a/services/registration/select_gender.vis b/services/registration/select_gender.vis index 25e53d3..1082cef 100644 --- a/services/registration/select_gender.vis +++ b/services/registration/select_gender.vis @@ -1,13 +1,15 @@ CATCH incorrect_pin flag_incorrect_pin 1 CATCH profile_update_success flag_allow_update 1 -LOAD save_gender 0 MOUT male 1 MOUT female 2 MOUT unspecified 3 MOUT back 0 HALT -RELOAD save_gender INCMP _ 0 -INCMP pin_entry * +INCMP set_male 1 +INCMP set_female 2 +INCMP set_unspecified 3 + + diff --git a/services/registration/select_language.vis b/services/registration/select_language.vis index aa83e0c..54f08e9 100644 --- a/services/registration/select_language.vis +++ b/services/registration/select_language.vis @@ -1,6 +1,6 @@ MOUT english 0 MOUT kiswahili 1 HALT -INCMP set_default 0 +INCMP set_eng 0 INCMP set_swa 1 INCMP . * diff --git a/services/registration/set_default.vis b/services/registration/set_eng.vis similarity index 100% rename from services/registration/set_default.vis rename to services/registration/set_eng.vis diff --git a/services/registration/set_female.vis b/services/registration/set_female.vis new file mode 100644 index 0000000..723b080 --- /dev/null +++ b/services/registration/set_female.vis @@ -0,0 +1,4 @@ +LOAD save_gender 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH profile_update_success flag_allow_update 1 +MOVE pin_entry diff --git a/services/registration/set_male.vis b/services/registration/set_male.vis new file mode 100644 index 0000000..723b080 --- /dev/null +++ b/services/registration/set_male.vis @@ -0,0 +1,4 @@ +LOAD save_gender 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH profile_update_success flag_allow_update 1 +MOVE pin_entry diff --git a/services/registration/set_unspecified.vis b/services/registration/set_unspecified.vis new file mode 100644 index 0000000..723b080 --- /dev/null +++ b/services/registration/set_unspecified.vis @@ -0,0 +1,4 @@ +LOAD save_gender 0 +CATCH incorrect_pin flag_incorrect_pin 1 +CATCH profile_update_success flag_allow_update 1 +MOVE pin_entry From 0dea34daab9517ffd54f8cb2a328d1cfbe72f064 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 09:18:21 +0300 Subject: [PATCH 12/34] resolve confilict --- internal/handlers/handlerservice.go | 72 ++++++++++++++--------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index c507d35..4cedd26 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -57,42 +57,42 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { if err != nil { return nil, err } - ussdHandlers = ussdHandlers.WithPersister(localHandlerService.Pe) - localHandlerService.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) - localHandlerService.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) - localHandlerService.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) - localHandlerService.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) - localHandlerService.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) - localHandlerService.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) - localHandlerService.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) - localHandlerService.DbRs.AddLocalFunc("quit", ussdHandlers.Quit) - localHandlerService.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) - localHandlerService.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) - localHandlerService.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) - localHandlerService.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) - localHandlerService.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) - localHandlerService.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) - localHandlerService.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) - localHandlerService.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender) - localHandlerService.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) - localHandlerService.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) - localHandlerService.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) - localHandlerService.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) - localHandlerService.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) - localHandlerService.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) - localHandlerService.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) - localHandlerService.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) - localHandlerService.DbRs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) - localHandlerService.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) - localHandlerService.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) - localHandlerService.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) - localHandlerService.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) - localHandlerService.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) - localHandlerService.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) - localHandlerService.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) - localHandlerService.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) - localHandlerService.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) - localHandlerService.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) + ussdHandlers = ussdHandlers.WithPersister(ls.Pe) + ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) + ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) + ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) + ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) + ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) + ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) + ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) + ls.DbRs.AddLocalFunc("quit", ussdHandlers.Quit) + ls.DbRs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) + ls.DbRs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) + ls.DbRs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) + ls.DbRs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) + ls.DbRs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) + ls.DbRs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) + ls.DbRs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) + ls.DbRs.AddLocalFunc("get_sender", ussdHandlers.GetSender) + ls.DbRs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) + ls.DbRs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) + ls.DbRs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) + ls.DbRs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) + ls.DbRs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) + ls.DbRs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) + ls.DbRs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) + ls.DbRs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) + ls.DbRs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) + ls.DbRs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) + ls.DbRs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) + ls.DbRs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) + ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) + ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) + ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) + ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) + ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) + ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) + ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) return ussdHandlers, nil } From 6ad67f6adc97095d08b35028ef94e08b6e0103a0 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 23 Sep 2024 11:38:07 +0300 Subject: [PATCH 13/34] change position of LOAD --- services/registration/transaction_pin.vis | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/registration/transaction_pin.vis b/services/registration/transaction_pin.vis index cadbdcf..a0941ed 100644 --- a/services/registration/transaction_pin.vis +++ b/services/registration/transaction_pin.vis @@ -5,8 +5,8 @@ RELOAD get_sender MAP get_sender MOUT back 0 MOUT quit 9 -HALT LOAD authorize_account 6 +HALT RELOAD authorize_account CATCH incorrect_pin flag_incorrect_pin 1 INCMP _ 0 From 4db862bc97147eac7609f31fa4b20b20ca3cab7c Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 23 Sep 2024 14:13:26 +0300 Subject: [PATCH 14/34] LOAD and RELOAD get_amount --- services/registration/transaction_initiated.vis | 2 +- services/registration/transaction_pin | 2 +- services/registration/transaction_pin.vis | 3 ++- services/registration/transaction_pin_swa | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/services/registration/transaction_initiated.vis b/services/registration/transaction_initiated.vis index 3b8b9f6..f8cf19c 100644 --- a/services/registration/transaction_initiated.vis +++ b/services/registration/transaction_initiated.vis @@ -1,7 +1,7 @@ LOAD reset_incorrect 6 CATCH incorrect_pin flag_incorrect_pin 1 CATCH _ flag_account_authorized 0 -LOAD get_amount 10 +RELOAD get_amount MAP get_amount RELOAD get_recipient MAP get_recipient diff --git a/services/registration/transaction_pin b/services/registration/transaction_pin index 7b6bddb..a1b7125 100644 --- a/services/registration/transaction_pin +++ b/services/registration/transaction_pin @@ -1,2 +1,2 @@ -{{.get_recipient}} will receive {{.validate_amount}} from {{.get_sender}} +{{.get_recipient}} will receive {{.get_amount}} from {{.get_sender}} Please enter your PIN to confirm: \ No newline at end of file diff --git a/services/registration/transaction_pin.vis b/services/registration/transaction_pin.vis index a0941ed..0388f0c 100644 --- a/services/registration/transaction_pin.vis +++ b/services/registration/transaction_pin.vis @@ -1,4 +1,5 @@ -MAP validate_amount +RELOAD get_amount +MAP get_amount RELOAD get_recipient MAP get_recipient RELOAD get_sender diff --git a/services/registration/transaction_pin_swa b/services/registration/transaction_pin_swa index 8529f0e..1924166 100644 --- a/services/registration/transaction_pin_swa +++ b/services/registration/transaction_pin_swa @@ -1,2 +1,2 @@ -{{.get_recipient}} atapokea {{.validate_amount}} kutoka kwa {{.get_sender}} +{{.get_recipient}} atapokea {{.get_amount}} kutoka kwa {{.get_sender}} Tafadhali weka PIN yako kudhibitisha: \ No newline at end of file From b15ba367c468571e32d3dde6d795507b6dfad2e2 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 23 Sep 2024 14:16:01 +0300 Subject: [PATCH 15/34] change position of LOAD function --- services/registration/amount.vis | 1 + services/registration/change_language.vis | 1 - services/registration/main.vis | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/services/registration/amount.vis b/services/registration/amount.vis index 9b2970a..b491fab 100644 --- a/services/registration/amount.vis +++ b/services/registration/amount.vis @@ -9,4 +9,5 @@ CATCH invalid_amount flag_invalid_amount 1 INCMP _ 0 LOAD get_recipient 12 LOAD get_sender 64 +LOAD get_amount 12 INCMP transaction_pin * diff --git a/services/registration/change_language.vis b/services/registration/change_language.vis index 05ca95b..b33acf1 100644 --- a/services/registration/change_language.vis +++ b/services/registration/change_language.vis @@ -1,4 +1,3 @@ -LOAD reset_account_authorized 0 LOAD reset_incorrect 0 CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 diff --git a/services/registration/main.vis b/services/registration/main.vis index d883dca..4e27a79 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,3 +1,4 @@ +LOAD reset_account_authorized 0 LOAD check_balance 64 RELOAD check_balance MAP check_balance From 77e4c5d43a6c67f2db8fc9f7ea0add7f8f0fed62 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 23 Sep 2024 21:20:57 +0300 Subject: [PATCH 16/34] reset the positon of LOAD function --- services/registration/change_language.vis | 1 + services/registration/main.vis | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/services/registration/change_language.vis b/services/registration/change_language.vis index b33acf1..05ca95b 100644 --- a/services/registration/change_language.vis +++ b/services/registration/change_language.vis @@ -1,3 +1,4 @@ +LOAD reset_account_authorized 0 LOAD reset_incorrect 0 CATCH incorrect_pin flag_incorrect_pin 1 CATCH pin_entry flag_account_authorized 0 diff --git a/services/registration/main.vis b/services/registration/main.vis index 4e27a79..d883dca 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,4 +1,3 @@ -LOAD reset_account_authorized 0 LOAD check_balance 64 RELOAD check_balance MAP check_balance From 8648ea599cadba0b3c3de1f010346acad84c6f9d Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 13:37:14 +0300 Subject: [PATCH 17/34] add catch node --- services/registration/_catch | 1 + services/registration/_catch.vis | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 services/registration/_catch create mode 100644 services/registration/_catch.vis diff --git a/services/registration/_catch b/services/registration/_catch new file mode 100644 index 0000000..e81b8e9 --- /dev/null +++ b/services/registration/_catch @@ -0,0 +1 @@ +Something went wrong.Please try again \ No newline at end of file diff --git a/services/registration/_catch.vis b/services/registration/_catch.vis new file mode 100644 index 0000000..0d20253 --- /dev/null +++ b/services/registration/_catch.vis @@ -0,0 +1,2 @@ +HALT +MOVE ^ From cc760e7698aa60011a4cefd12cd1cdfa3f602258 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Tue, 24 Sep 2024 21:21:35 +0300 Subject: [PATCH 18/34] remove move --- services/registration/_catch.vis | 1 - 1 file changed, 1 deletion(-) diff --git a/services/registration/_catch.vis b/services/registration/_catch.vis index 0d20253..72e55ad 100644 --- a/services/registration/_catch.vis +++ b/services/registration/_catch.vis @@ -1,2 +1 @@ HALT -MOVE ^ From 6dbd2506946dac86aaf860f0acd64afc32757fa3 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Wed, 25 Sep 2024 09:29:26 +0300 Subject: [PATCH 19/34] remove extra spaces --- internal/utils/isocode.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/utils/isocode.go b/internal/utils/isocode.go index 34365b0..3bdfbeb 100644 --- a/internal/utils/isocode.go +++ b/internal/utils/isocode.go @@ -1,8 +1,5 @@ package utils - - - var isoCodes = map[string]bool{ "eng": true, // English "swa": true, // Swahili @@ -12,4 +9,3 @@ var isoCodes = map[string]bool{ func IsValidISO639(code string) bool { return isoCodes[code] } - From c220cbb767c0678d5d7a40f1d0ca053824fe77d5 Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 26 Sep 2024 15:05:17 +0100 Subject: [PATCH 20/34] Revert accidental merge of ssh --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 54ad3ff..ddccccf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ go.work* **/*/*.bin **/*/.state/ cmd/.state/ +id_* +*.gdbm From 4ee241714bd51d8fbfbd416319a2ebe375765df4 Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 26 Sep 2024 15:08:15 +0100 Subject: [PATCH 21/34] Attempt 2 revert changes from ssh --- cmd/ssh/README.md | 34 ----- cmd/ssh/main.go | 115 ---------------- cmd/ssh/sshkey/main.go | 44 ------ internal/ssh/keystore.go | 64 --------- internal/ssh/ssh.go | 284 --------------------------------------- 5 files changed, 541 deletions(-) delete mode 100644 cmd/ssh/README.md delete mode 100644 cmd/ssh/main.go delete mode 100644 cmd/ssh/sshkey/main.go delete mode 100644 internal/ssh/keystore.go delete mode 100644 internal/ssh/ssh.go diff --git a/cmd/ssh/README.md b/cmd/ssh/README.md deleted file mode 100644 index ff325d7..0000000 --- a/cmd/ssh/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# URDT-USSD SSH server - -An SSH server entry point for the vise engine. - - -## Adding public keys for access - -Map your (client) public key to a session identifier (e.g. phone number) - -``` -go run -v -tags logtrace ./cmd/ssh/sshkey/main.go -i [--dbdir ] -``` - - -## Create a private key for the server - -``` -ssh-keygen -N "" -f -``` - - -## Run the server - - -``` -go run -v -tags logtrace ./cmd/ssh/main.go -h -p [--dbdir ] -``` - - -## Connect to the server - -``` -ssh [-v] -T -p -i -``` diff --git a/cmd/ssh/main.go b/cmd/ssh/main.go deleted file mode 100644 index 0227616..0000000 --- a/cmd/ssh/main.go +++ /dev/null @@ -1,115 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "path" - "os" - "os/signal" - "sync" - "syscall" - - "git.defalsify.org/vise.git/db" - "git.defalsify.org/vise.git/engine" - "git.defalsify.org/vise.git/logging" - - "git.grassecon.net/urdt/ussd/internal/ssh" -) - -var ( - wg sync.WaitGroup - keyStore db.Db - logg = logging.NewVanilla() - scriptDir = path.Join("services", "registration") -) - -func main() { - var dbDir string - var resourceDir string - var size uint - var engineDebug bool - var stateDebug bool - var host string - var port uint - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") - flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir") - flag.BoolVar(&engineDebug, "engine-debug", false, "use engine debug output") - flag.BoolVar(&stateDebug, "state-debug", false, "use engine debug output") - flag.UintVar(&size, "s", 160, "max size of output") - flag.StringVar(&host, "h", "127.0.0.1", "http host") - flag.UintVar(&port, "p", 7122, "http port") - flag.Parse() - - sshKeyFile := flag.Arg(0) - _, err := os.Stat(sshKeyFile) - if err != nil { - fmt.Fprintf(os.Stderr, "cannot open ssh server private key file: %v\n", err) - os.Exit(1) - } - - ctx := context.Background() - logg.WarnCtxf(ctx, "!!!!! WARNING WARNING WARNING") - logg.WarnCtxf(ctx, "!!!!! =======================") - logg.WarnCtxf(ctx, "!!!!! This is not a production ready server!") - logg.WarnCtxf(ctx, "!!!!! Do not expose to internet and only use with tunnel!") - logg.WarnCtxf(ctx, "!!!!! (See ssh -L <...>)") - - logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port) - - pfp := path.Join(scriptDir, "pp.csv") - - cfg := engine.Config{ - Root: "root", - OutputSize: uint32(size), - FlagCount: uint32(16), - } - if stateDebug { - cfg.StateDebug = true - } - if engineDebug { - cfg.EngineDebug = true - } - - authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir) - if err != nil { - fmt.Fprintf(os.Stderr, "keystore file open error: %v", err) - os.Exit(1) - } - defer func () { - logg.TraceCtxf(ctx, "shutdown auth key store reached") - err = authKeyStore.Close() - if err != nil { - logg.ErrorCtxf(ctx, "keystore close error", "err", err) - } - }() - - cint := make(chan os.Signal) - cterm := make(chan os.Signal) - signal.Notify(cint, os.Interrupt, syscall.SIGINT) - signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) - - runner := &ssh.SshRunner{ - Cfg: cfg, - Debug: engineDebug, - FlagFile: pfp, - DbDir: dbDir, - ResourceDir: resourceDir, - SrvKeyFile: sshKeyFile, - Host: host, - Port: port, - } - go func() { - select { - case _ = <-cint: - case _ = <-cterm: - } - logg.TraceCtxf(ctx, "shutdown runner reached") - err := runner.Stop() - if err != nil { - logg.ErrorCtxf(ctx, "runner stop error", "err", err) - } - - }() - runner.Run(ctx, authKeyStore) -} diff --git a/cmd/ssh/sshkey/main.go b/cmd/ssh/sshkey/main.go deleted file mode 100644 index 87b89a3..0000000 --- a/cmd/ssh/sshkey/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "os" - - "git.grassecon.net/urdt/ussd/internal/ssh" -) - -func main() { - var dbDir string - var sessionId string - flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from") - flag.StringVar(&sessionId, "i", "", "session id") - flag.Parse() - - if sessionId == "" { - fmt.Fprintf(os.Stderr, "empty session id\n") - os.Exit(1) - } - - ctx := context.Background() - - sshKeyFile := flag.Arg(0) - if sshKeyFile == "" { - fmt.Fprintf(os.Stderr, "missing key file argument\n") - os.Exit(1) - } - - store, err := ssh.NewSshKeyStore(ctx, dbDir) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } - defer store.Close() - - err = store.AddFromFile(ctx, sshKeyFile, sessionId) - if err != nil { - fmt.Fprintf(os.Stderr, "%v\n", err) - os.Exit(1) - } -} diff --git a/internal/ssh/keystore.go b/internal/ssh/keystore.go deleted file mode 100644 index fed7233..0000000 --- a/internal/ssh/keystore.go +++ /dev/null @@ -1,64 +0,0 @@ -package ssh - -import ( - "context" - "fmt" - "os" - "path" - - "golang.org/x/crypto/ssh" - - "git.defalsify.org/vise.git/db" - - "git.grassecon.net/urdt/ussd/internal/storage" -) - -type SshKeyStore struct { - store db.Db -} - -func NewSshKeyStore(ctx context.Context, dbDir string) (*SshKeyStore, error) { - keyStore := &SshKeyStore{} - keyStoreFile := path.Join(dbDir, "ssh_authorized_keys.gdbm") - keyStore.store = storage.NewThreadGdbmDb() - err := keyStore.store.Connect(ctx, keyStoreFile) - if err != nil { - return nil, err - } - return keyStore, nil -} - -func(s *SshKeyStore) AddFromFile(ctx context.Context, fp string, sessionId string) error { - _, err := os.Stat(fp) - if err != nil { - return fmt.Errorf("cannot open ssh server public key file: %v\n", err) - } - - publicBytes, err := os.ReadFile(fp) - if err != nil { - return fmt.Errorf("Failed to load public key: %v", err) - } - pubKey, _, _, _, err := ssh.ParseAuthorizedKey(publicBytes) - if err != nil { - return fmt.Errorf("Failed to parse public key: %v", err) - } - k := append([]byte{0x01}, pubKey.Marshal()...) - s.store.SetPrefix(storage.DATATYPE_CUSTOM) - logg.Infof("Added key", "sessionId", sessionId, "public key", string(publicBytes)) - return s.store.Put(ctx, k, []byte(sessionId)) -} - -func(s *SshKeyStore) Get(ctx context.Context, pubKey ssh.PublicKey) (string, error) { - s.store.SetLanguage(nil) - s.store.SetPrefix(storage.DATATYPE_CUSTOM) - k := append([]byte{0x01}, pubKey.Marshal()...) - v, err := s.store.Get(ctx, k) - if err != nil { - return "", err - } - return string(v), nil -} - -func(s *SshKeyStore) Close() error { - return s.store.Close() -} diff --git a/internal/ssh/ssh.go b/internal/ssh/ssh.go deleted file mode 100644 index 394f55f..0000000 --- a/internal/ssh/ssh.go +++ /dev/null @@ -1,284 +0,0 @@ -package ssh - -import ( - "context" - "encoding/hex" - "encoding/base64" - "errors" - "fmt" - "net" - "os" - "sync" - - "golang.org/x/crypto/ssh" - - "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 *SshKeyStore - auth map[string]string -} - -func NewAuther(ctx context.Context, keyStore *SshKeyStore) *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) { - va, err := a.keyStore.Get(a.Ctx, pubKey) - if err != nil { - return nil, err - } - ka := hex.EncodeToString(conn.SessionID()) - a.auth[ka] = va - fmt.Fprintf(os.Stderr, "connect: %s -> %s\n", ka, va) - 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 - lst net.Listener -} - -func(s *SshRunner) Stop() error { - return s.lst.Close() -} - -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 *SshKeyStore) { - 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) - } - srvPub := private.PublicKey() - srvPubStr := base64.StdEncoding.EncodeToString(srvPub.Marshal()) - logg.InfoCtxf(ctx, "have server key", "type", srvPub.Type(), "public", srvPubStr) - cfg.AddHostKey(private) - - s.lst, err = net.Listen("tcp", fmt.Sprintf("%s:%d", s.Host, s.Port)) - if err != nil { - panic(err) - } - - for running { - conn, err := s.lst.Accept() - if err != nil { - logg.ErrorCtxf(ctx, "ssh accept error", "err", err) - running = false - continue - } - - 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) - } -} From a993026380c511fc830fb4505d674bba01e0c973 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Thu, 26 Sep 2024 23:07:48 +0300 Subject: [PATCH 22/34] update tests --- internal/handlers/ussd/menuhandler_test.go | 159 ++++++++------------- 1 file changed, 60 insertions(+), 99 deletions(-) diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index d0367f0..57767ef 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -9,6 +9,7 @@ import ( "testing" "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" "git.grassecon.net/urdt/ussd/internal/mocks" @@ -16,6 +17,7 @@ import ( "git.grassecon.net/urdt/ussd/internal/utils" "github.com/alecthomas/assert/v2" testdataloader "github.com/peteole/testdata-loader" + "github.com/stretchr/testify/require" ) var ( @@ -94,6 +96,28 @@ func TestCreateAccount(t *testing.T) { mockDataStore.AssertExpectations(t) } + + +func TestWithPersister(t *testing.T) { + // Test case: Setting a persister + h := &Handlers{} + p := &persist.Persister{} + + result := h.WithPersister(p) + + assert.Equal(t, p, h.pe, "The persister should be set correctly.") + assert.Equal(t, h, result, "The returned handler should be the same instance.") +} + +func TestWithPersister_PanicWhenAlreadySet(t *testing.T) { + // Test case: Panic on multiple calls + h := &Handlers{pe: &persist.Persister{}} + require.Panics(t, func() { + h.WithPersister(&persist.Persister{}) + }, "Should panic when trying to set a persister again.") +} + + func TestSaveFirstname(t *testing.T) { // Create a new instance of MockMyDataStore mockStore := new(mocks.MockUserDataStore) @@ -295,6 +319,7 @@ func TestSaveOfferings(t *testing.T) { func TestSaveGender(t *testing.T) { // Create a new instance of MockMyDataStore mockStore := new(mocks.MockUserDataStore) + mockState := state.NewState(16) // Define the session ID and context sessionId := "session123" @@ -302,35 +327,39 @@ func TestSaveGender(t *testing.T) { // Define test cases tests := []struct { - name string - input []byte - expectedGender string - expectCall bool + name string + input []byte + expectedGender string + expectCall bool + executingSymbol string }{ { - name: "Valid Male Input", - input: []byte("1"), - expectedGender: "Male", - expectCall: true, + name: "Valid Male Input", + input: []byte("1"), + expectedGender: "male", + executingSymbol: "set_male", + expectCall: true, }, { name: "Valid Female Input", input: []byte("2"), - expectedGender: "Female", + expectedGender: "female", + executingSymbol: "set_female", expectCall: true, }, { name: "Valid Unspecified Input", input: []byte("3"), - expectedGender: "Unspecified", + executingSymbol: "set_unspecified", + expectedGender: "unspecified", expectCall: true, }, - { - name: "Empty Input", - input: []byte(""), - expectedGender: "", - expectCall: false, - }, + // { + // name: "Empty Input", + // input: []byte(""), + // expectedGender: "", + // expectCall: false, + // }, } for _, tt := range tests { @@ -342,14 +371,15 @@ func TestSaveGender(t *testing.T) { } else { mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_GENDER, []byte(tt.expectedGender)).Return(nil) } - + mockState.ExecPath = append(mockState.ExecPath, tt.executingSymbol) // Create the Handlers instance with the mock store h := &Handlers{ userdataStore: mockStore, + st: mockState, } // Call the method - _, err := h.SaveGender(ctx, "someSym", tt.input) + _, err := h.SaveGender(ctx, "save_gender", tt.input) // Assert no error assert.NoError(t, err) @@ -538,13 +568,13 @@ func TestSetLanguage(t *testing.T) { } // Define test cases tests := []struct { - name string - execPath []string - expectedResult resource.Result + name string + execPath []string + expectedResult resource.Result }{ { name: "Set Default Language (English)", - execPath: []string{"set_default"}, + execPath: []string{"set_eng"}, expectedResult: resource.Result{ FlagSet: []uint32{state.FLAG_LANG, 8}, Content: "eng", @@ -558,13 +588,13 @@ func TestSetLanguage(t *testing.T) { Content: "swa", }, }, - { - name: "Unhandled path", - execPath: []string{""}, - expectedResult: resource.Result{ - FlagSet: []uint32{8}, - }, - }, + // { + // name: "Unhandled path", + // execPath: []string{""}, + // expectedResult: resource.Result{ + // FlagSet: []uint32{8}, + // }, + // }, } for _, tt := range tests { @@ -592,76 +622,6 @@ func TestSetLanguage(t *testing.T) { }) } } - -func TestSetResetSingleEdit(t *testing.T) { - fm, err := NewFlagManager(flagsPath) - - flag_allow_update, _ := fm.parser.GetFlag("flag_allow_update") - flag_single_edit, _ := fm.parser.GetFlag("flag_single_edit") - - if err != nil { - log.Fatal(err) - } - // Define test cases - tests := []struct { - name string - input []byte - expectedResult resource.Result - }{ - { - name: "Set single Edit", - input: []byte("2"), - expectedResult: resource.Result{ - FlagSet: []uint32{flag_single_edit}, - FlagReset: []uint32{flag_allow_update}, - }, - }, - { - name: "Set single Edit", - input: []byte("3"), - expectedResult: resource.Result{ - FlagSet: []uint32{flag_single_edit}, - FlagReset: []uint32{flag_allow_update}, - }, - }, - { - name: "Set single edit", - input: []byte("4"), - expectedResult: resource.Result{ - FlagReset: []uint32{flag_allow_update}, - FlagSet: []uint32{flag_single_edit}, - }, - }, - { - name: "No single edit set", - input: []byte("1"), - expectedResult: resource.Result{ - FlagReset: []uint32{flag_single_edit}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - - // Create the Handlers instance with the mock flag manager - h := &Handlers{ - flagManager: fm.parser, - } - - // Call the method - res, err := h.SetResetSingleEdit(context.Background(), "set_reset_single_edit", tt.input) - - if err != nil { - t.Error(err) - } - // Assert that the Result FlagSet has the required flags after language switch - assert.Equal(t, res, tt.expectedResult, "Flags should match reset edit") - - }) - } -} - func TestResetAllowUpdate(t *testing.T) { fm, err := NewFlagManager(flagsPath) @@ -1648,6 +1608,7 @@ func TestGetProfile(t *testing.T) { mockDataStore := new(mocks.MockUserDataStore) mockCreateAccountService := new(mocks.MockAccountService) + h := &Handlers{ userdataStore: mockDataStore, accountService: mockCreateAccountService, From ea52a7cf0a434f1322331f0ce14cd5710f1f34bc Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Thu, 26 Sep 2024 23:11:37 +0300 Subject: [PATCH 23/34] update tests --- internal/handlers/ussd/menuhandler_test.go | 44 +++++++++++----------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 57767ef..283104a 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -1440,7 +1440,7 @@ func TestValidateAmount(t *testing.T) { if err != nil { t.Logf(err.Error()) } - //flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") + flag_invalid_amount, _ := fm.parser.GetFlag("flag_invalid_amount") mockDataStore := new(mocks.MockUserDataStore) mockCreateAccountService := new(mocks.MockAccountService) @@ -1469,26 +1469,26 @@ func TestValidateAmount(t *testing.T) { Content: "0.001", }, }, - // { - // name: "Test with amount larger than balance", - // input: []byte("0.02"), - // balance: "0.003 CELO", - // publicKey: []byte("0xrqeqrequuq"), - // expectedResult: resource.Result{ - // FlagSet: []uint32{flag_invalid_amount}, - // Content: "0.02", - // }, - // }, - // { - // name: "Test with invalid amount", - // input: []byte("0.02ms"), - // balance: "0.003 CELO", - // publicKey: []byte("0xrqeqrequuq"), - // expectedResult: resource.Result{ - // FlagSet: []uint32{flag_invalid_amount}, - // Content: "0.02ms", - // }, - // }, + { + name: "Test with amount larger than balance", + input: []byte("0.02"), + balance: "0.003 CELO", + publicKey: []byte("0xrqeqrequuq"), + expectedResult: resource.Result{ + FlagSet: []uint32{flag_invalid_amount}, + Content: "0.02", + }, + }, + { + name: "Test with invalid amount", + input: []byte("0.02ms"), + balance: "0.003 CELO", + publicKey: []byte("0xrqeqrequuq"), + expectedResult: resource.Result{ + FlagSet: []uint32{flag_invalid_amount}, + Content: "0.02ms", + }, + }, } for _, tt := range tests { @@ -1496,7 +1496,7 @@ func TestValidateAmount(t *testing.T) { mockDataStore.On("ReadEntry", ctx, sessionId, utils.DATA_PUBLIC_KEY).Return(tt.publicKey, nil) mockCreateAccountService.On("CheckBalance", string(tt.publicKey)).Return(tt.balance, nil) - mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil) + mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_AMOUNT, tt.input).Return(nil).Maybe() // Call the method under test res, _ := h.ValidateAmount(ctx, "test_validate_amount", tt.input) From a3ff3be5b17549a8178d69da6c0312a4c94bf9b4 Mon Sep 17 00:00:00 2001 From: Carlosokumu Date: Fri, 27 Sep 2024 16:32:47 +0300 Subject: [PATCH 24/34] clean up --- internal/handlers/ussd/menuhandler_test.go | 32 ++++++---------------- 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 283104a..83e6f0c 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -96,8 +96,6 @@ func TestCreateAccount(t *testing.T) { mockDataStore.AssertExpectations(t) } - - func TestWithPersister(t *testing.T) { // Test case: Setting a persister h := &Handlers{} @@ -117,7 +115,6 @@ func TestWithPersister_PanicWhenAlreadySet(t *testing.T) { }, "Should panic when trying to set a persister again.") } - func TestSaveFirstname(t *testing.T) { // Create a new instance of MockMyDataStore mockStore := new(mocks.MockUserDataStore) @@ -341,25 +338,19 @@ func TestSaveGender(t *testing.T) { expectCall: true, }, { - name: "Valid Female Input", - input: []byte("2"), - expectedGender: "female", + name: "Valid Female Input", + input: []byte("2"), + expectedGender: "female", executingSymbol: "set_female", - expectCall: true, + expectCall: true, }, { - name: "Valid Unspecified Input", - input: []byte("3"), + name: "Valid Unspecified Input", + input: []byte("3"), executingSymbol: "set_unspecified", - expectedGender: "unspecified", - expectCall: true, + expectedGender: "unspecified", + expectCall: true, }, - // { - // name: "Empty Input", - // input: []byte(""), - // expectedGender: "", - // expectCall: false, - // }, } for _, tt := range tests { @@ -588,13 +579,6 @@ func TestSetLanguage(t *testing.T) { Content: "swa", }, }, - // { - // name: "Unhandled path", - // execPath: []string{""}, - // expectedResult: resource.Result{ - // FlagSet: []uint32{8}, - // }, - // }, } for _, tt := range tests { From 4c339450810a8ee212b2fe464fac847f567f7ad8 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Fri, 27 Sep 2024 19:10:08 +0300 Subject: [PATCH 25/34] use latest go-vise update --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c4c5167..e3a441a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd go 1.22.6 require ( - git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb + git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac github.com/alecthomas/assert/v2 v2.2.2 github.com/peteole/testdata-loader v0.3.0 gopkg.in/leonelquinteros/gotext.v1 v1.3.1 diff --git a/go.sum b/go.sum index ed5636f..3f7a262 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb h1:6P4kxihcwMjDKzvUFC6t2zGNb7MDW+l/ACGlSAN1N8Y= git.defalsify.org/vise.git v0.1.0-rc.3.0.20240923162317-c20d557a3dbb/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac h1:D4KI22KWXT8S66sHIjWhTBX6SXRfnd7j8VErq3PPbok= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240926120105-89b0529cf7ac/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk= github.com/alecthomas/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g= From 1dc8b054eb3e18b153fce9f72b13476e29aca141 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 27 Sep 2024 21:10:03 +0100 Subject: [PATCH 26/34] Add sub-prefix db wrapper --- internal/storage/db.go | 43 +++++++++++++++++++++++++++++ internal/storage/db_test.go | 54 +++++++++++++++++++++++++++++++++++++ internal/storage/storage.go | 2 +- 3 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 internal/storage/db.go create mode 100644 internal/storage/db_test.go diff --git a/internal/storage/db.go b/internal/storage/db.go new file mode 100644 index 0000000..b2ac6a9 --- /dev/null +++ b/internal/storage/db.go @@ -0,0 +1,43 @@ +package storage + +import ( + "context" + + "git.defalsify.org/vise.git/db" +) + +const ( + DATATYPE_USERSUB = 64 +) + +type SubPrefixDb struct { + store db.Db + pfx []byte +} + +func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { + return &SubPrefixDb{ + store: store, + pfx: pfx, + } +} + +func(s *SubPrefixDb) toKey(k []byte) []byte { + return append(s.pfx, k...) +} + +func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { + s.store.SetPrefix(DATATYPE_USERSUB) + key = s.toKey(key) + v, err := s.store.Get(ctx, key) + if err != nil { + return nil, err + } + return v, nil +} + +func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { + s.store.SetPrefix(DATATYPE_USERSUB) + key = s.toKey(key) + return s.store.Put(ctx, key, val) +} diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go new file mode 100644 index 0000000..9e2183c --- /dev/null +++ b/internal/storage/db_test.go @@ -0,0 +1,54 @@ +package storage + +import ( + "bytes" + "context" + "testing" + + memdb "git.defalsify.org/vise.git/db/mem" +) + +func TestSubPrefix(t *testing.T) { + ctx := context.Background() + db := memdb.NewMemDb() + err := db.Connect(ctx, "") + if err != nil { + t.Fatal(err) + } + sdba := NewSubPrefixDb(db, []byte("tinkywinky")) + err = sdba.Put(ctx, []byte("foo"), []byte("dipsy")) + if err != nil { + t.Fatal(err) + } + + r, err := sdba.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(r, []byte("dipsy")) { + t.Fatalf("expected 'dipsy', got %s", r) + } + + sdbb := NewSubPrefixDb(db, []byte("lala")) + r, err = sdbb.Get(ctx, []byte("foo")) + if err == nil { + t.Fatal("expected not found") + } + + err = sdbb.Put(ctx, []byte("foo"), []byte("pu")) + if err != nil { + t.Fatal(err) + } + r, err = sdbb.Get(ctx, []byte("foo")) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(r, []byte("pu")) { + t.Fatalf("expected 'pu', got %s", r) + } + + r, err = sdba.Get(ctx, []byte("foo")) + if !bytes.Equal(r, []byte("dipsy")) { + t.Fatalf("expected 'dipsy', got %s", r) + } +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 009819e..8fc92f4 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -6,7 +6,7 @@ import ( ) const ( - DATATYPE_CUSTOM = 128 + DATATYPE_EXTEND = 128 ) type Storage struct { From 2a93ea7a0c78a4f4e5bff5524aa1b2f95fe54953 Mon Sep 17 00:00:00 2001 From: lash Date: Fri, 27 Sep 2024 21:16:08 +0100 Subject: [PATCH 27/34] Remove out-of-context extend key --- internal/storage/storage.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 8fc92f4..53f4392 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -5,10 +5,6 @@ import ( "git.defalsify.org/vise.git/persist" ) -const ( - DATATYPE_EXTEND = 128 -) - type Storage struct { Persister *persist.Persister UserdataDb db.Db From 3a46fda769941e196b0e62ad12a4924782de9dd1 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Sat, 28 Sep 2024 12:26:37 +0300 Subject: [PATCH 28/34] use save_temporary_pin and updated tests --- internal/handlers/handlerservice.go | 7 +-- internal/handlers/ussd/menuhandler.go | 59 +++++++------------- internal/handlers/ussd/menuhandler_test.go | 51 +++-------------- services/registration/account_creation.vis | 2 +- services/registration/confirm_create_pin.vis | 4 +- services/registration/create_pin.vis | 4 +- 6 files changed, 38 insertions(+), 89 deletions(-) diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 3dffe16..1a556cc 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -60,8 +60,8 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { ussdHandlers = ussdHandlers.WithPersister(ls.Pe) ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage) ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) - ls.DbRs.AddLocalFunc("save_pin", ussdHandlers.SavePin) - ls.DbRs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) + ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) + ls.DbRs.AddLocalFunc("verify_create_pin", ussdHandlers.VerifyCreatePin) ls.DbRs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) ls.DbRs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) ls.DbRs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) @@ -89,11 +89,10 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { ls.DbRs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) ls.DbRs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) ls.DbRs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) - ls.DbRs.AddLocalFunc("save_temporary_pin", ussdHandlers.SaveTemporaryPin) ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) - ls.DbRs.AddLocalFunc("get_vouchers",ussdHandlers.GetVoucherList) + ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) return ussdHandlers, nil } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 5ebdaa5..165a456 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -180,34 +180,6 @@ func (h *Handlers) CreateAccount(ctx context.Context, sym string, input []byte) return res, nil } -// SavePin persists the user's PIN choice into the filesystem -func (h *Handlers) SavePin(ctx context.Context, sym string, input []byte) (resource.Result, error) { - var res resource.Result - var err error - - sessionId, ok := ctx.Value("SessionId").(string) - if !ok { - return res, fmt.Errorf("missing session") - } - - flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") - - accountPIN := string(input) - // Validate that the PIN is a 4-digit number - if !isValidPIN(accountPIN) { - res.FlagSet = append(res.FlagSet, flag_incorrect_pin) - return res, nil - } - - res.FlagReset = append(res.FlagReset, flag_incorrect_pin) - store := h.userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(accountPIN)) - if err != nil { - return res, err - } - return res, nil -} - func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { res := resource.Result{} _, ok := ctx.Value("SessionId").(string) @@ -226,6 +198,9 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) ( return res, nil } +// SaveTemporaryPin saves the valid PIN input to the DATA_TEMPORARY_PIN +// during the account creation process +// and during the change PIN process func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result var err error @@ -234,6 +209,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt if !ok { return res, fmt.Errorf("missing session") } + flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin") accountPIN := string(input) @@ -243,16 +219,19 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt res.FlagSet = append(res.FlagSet, flag_incorrect_pin) return res, nil } + + res.FlagReset = append(res.FlagReset, flag_incorrect_pin) + store := h.userdataStore err = store.WriteEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(accountPIN)) if err != nil { return res, err } + return res, nil } - -func (h *Handlers) GetVoucherList(ctx context.Context,sym string,input []byte) (resource.Result,error){ +func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result vouchers := []string{ "SRF", @@ -283,9 +262,9 @@ func (h *Handlers) GetVoucherList(ctx context.Context,sym string,input []byte) ( for i, voucher := range vouchers { numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher)) } - res.Content = strings.Join(numberedVouchers,"\n") + res.Content = strings.Join(numberedVouchers, "\n") - return res,nil + return res, nil } func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byte) (resource.Result, error) { @@ -313,10 +292,10 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt return res, nil } -// VerifyPin checks whether the confirmation PIN is similar to the account PIN -// If similar, it sets the USERFLAG_PIN_SET flag allowing the user +// VerifyCreatePin checks whether the confirmation PIN is similar to the temporary PIN +// If similar, it sets the USERFLAG_PIN_SET flag and writes the account PIN allowing the user // to access the main menu -func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (resource.Result, error) { +func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin") @@ -328,14 +307,13 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res return res, fmt.Errorf("missing session") } - //AccountPin, _ := utils.ReadEntry(ctx, h.userdataStore, sessionId, utils.DATA_ACCOUNT_PIN) store := h.userdataStore - AccountPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN) + temporaryPin, err := store.ReadEntry(ctx, sessionId, utils.DATA_TEMPORARY_PIN) if err != nil { return res, err } - if bytes.Equal(input, AccountPin) { + if bytes.Equal(input, temporaryPin) { res.FlagSet = []uint32{flag_valid_pin} res.FlagReset = []uint32{flag_pin_mismatch} res.FlagSet = append(res.FlagSet, flag_pin_set) @@ -343,6 +321,11 @@ func (h *Handlers) VerifyPin(ctx context.Context, sym string, input []byte) (res res.FlagSet = []uint32{flag_pin_mismatch} } + err = store.WriteEntry(ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(temporaryPin)) + if err != nil { + return res, err + } + return res, nil } diff --git a/internal/handlers/ussd/menuhandler_test.go b/internal/handlers/ussd/menuhandler_test.go index 83e6f0c..1ce8b51 100644 --- a/internal/handlers/ussd/menuhandler_test.go +++ b/internal/handlers/ussd/menuhandler_test.go @@ -171,7 +171,7 @@ func TestSaveFamilyname(t *testing.T) { mockStore.AssertExpectations(t) } -func TestSavePin(t *testing.T) { +func TestSaveTemporaryPIn(t *testing.T) { fm, err := NewFlagManager(flagsPath) mockStore := new(mocks.MockUserDataStore) if err != nil { @@ -213,10 +213,10 @@ func TestSavePin(t *testing.T) { t.Run(tt.name, func(t *testing.T) { // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(tt.input)).Return(nil) + mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(tt.input)).Return(nil) // Call the method - res, err := h.SavePin(ctx, "save_pin", tt.input) + res, err := h.SaveTemporaryPin(ctx, "save_pin", tt.input) if err != nil { t.Error(err) @@ -937,7 +937,7 @@ func TestVerifyYob(t *testing.T) { } } -func TestVerifyPin(t *testing.T) { +func TestVerifyCreatePin(t *testing.T) { fm, err := NewFlagManager(flagsPath) if err != nil { @@ -986,7 +986,7 @@ func TestVerifyPin(t *testing.T) { }, } - typ := utils.DATA_ACCOUNT_PIN + typ := utils.DATA_TEMPORARY_PIN for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -994,8 +994,11 @@ func TestVerifyPin(t *testing.T) { // Define expected interactions with the mock mockDataStore.On("ReadEntry", ctx, sessionId, typ).Return([]byte(firstSetPin), nil) + // Set up the expected behavior of the mock + mockDataStore.On("WriteEntry", ctx, sessionId, utils.DATA_ACCOUNT_PIN, []byte(firstSetPin)).Return(nil) + // Call the method under test - res, err := h.VerifyPin(ctx, "verify_pin", []byte(tt.input)) + res, err := h.VerifyCreatePin(ctx, "verify_create_pin", []byte(tt.input)) // Assert that no errors occurred assert.NoError(t, err) @@ -1693,42 +1696,6 @@ func TestVerifyNewPin(t *testing.T) { } -func TestSaveTemporaryPIn(t *testing.T) { - - fm, err := NewFlagManager(flagsPath) - - if err != nil { - t.Logf(err.Error()) - } - - // Create a new instance of UserDataStore - mockStore := new(mocks.MockUserDataStore) - - // Define test data - sessionId := "session123" - PIN := "1234" - ctx := context.WithValue(context.Background(), "SessionId", sessionId) - - // Set up the expected behavior of the mock - mockStore.On("WriteEntry", ctx, sessionId, utils.DATA_TEMPORARY_PIN, []byte(PIN)).Return(nil) - - // Create the Handlers instance with the mock store - h := &Handlers{ - userdataStore: mockStore, - flagManager: fm.parser, - } - - // Call the method - res, err := h.SaveTemporaryPin(ctx, "save_temporary_pin", []byte(PIN)) - - // Assert results - assert.NoError(t, err) - assert.Equal(t, resource.Result{}, res) - - // Assert all expectations were met - mockStore.AssertExpectations(t) -} - func TestConfirmPin(t *testing.T) { sessionId := "session123" diff --git a/services/registration/account_creation.vis b/services/registration/account_creation.vis index f4f326b..380fe6d 100644 --- a/services/registration/account_creation.vis +++ b/services/registration/account_creation.vis @@ -1,4 +1,4 @@ -RELOAD verify_pin +RELOAD verify_create_pin CATCH create_pin_mismatch flag_pin_mismatch 1 LOAD quit 0 HALT diff --git a/services/registration/confirm_create_pin.vis b/services/registration/confirm_create_pin.vis index 1235916..1a3173c 100644 --- a/services/registration/confirm_create_pin.vis +++ b/services/registration/confirm_create_pin.vis @@ -1,4 +1,4 @@ -LOAD save_pin 0 +LOAD save_temporary_pin 0 HALT -LOAD verify_pin 8 +LOAD verify_create_pin 8 INCMP account_creation * diff --git a/services/registration/create_pin.vis b/services/registration/create_pin.vis index e0e330f..c76e1bf 100644 --- a/services/registration/create_pin.vis +++ b/services/registration/create_pin.vis @@ -2,8 +2,8 @@ LOAD create_account 0 CATCH account_creation_failed flag_account_creation_failed 1 MOUT exit 0 HALT -LOAD save_pin 0 -RELOAD save_pin +LOAD save_temporary_pin 0 +RELOAD save_temporary_pin CATCH . flag_incorrect_pin 1 INCMP quit 0 INCMP confirm_create_pin * From 31aea6b807ed352b44e7640a794bab87269dbe4e Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Sat, 28 Sep 2024 14:07:37 +0300 Subject: [PATCH 29/34] added model and func to fetch vouchers from the API --- internal/handlers/server/accountservice.go | 29 ++++++++++++++++++ internal/handlers/ussd/menuhandler.go | 35 +++++++--------------- internal/mocks/servicemock.go | 7 ++++- internal/models/vouchersresponse.go | 7 +++++ 4 files changed, 53 insertions(+), 25 deletions(-) create mode 100644 internal/models/vouchersresponse.go diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go index f4375a1..ca460c7 100644 --- a/internal/handlers/server/accountservice.go +++ b/internal/handlers/server/accountservice.go @@ -13,6 +13,7 @@ type AccountServiceInterface interface { CheckBalance(publicKey string) (string, error) CreateAccount() (*models.AccountResponse, error) CheckAccountStatus(trackingId string) (string, error) + FetchVouchersFromAPI() ([]models.VoucherHolding, error) } type AccountService struct { @@ -110,3 +111,31 @@ func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { return &accountResp, nil } + +// fetchVouchersFromAPI calls the API to get the list of vouchers belonging to the user +func (as *AccountService) FetchVouchersFromAPI() ([]models.VoucherHolding, error) { + // TODO replace with the actual request once ready + mockJSON := `[ + { + "symbol": "MUMO", + "address": "0x078b3a26596218507781722A4e8825BFB9570Fba" + }, + { + "symbol": "SRF", + "address": "0x45d747172e77d55575c197CbA9451bC2CD8F4958" + }, + { + "symbol": "HALGAN", + "address": "0x12169Fb5931A599ad1283bb8311Dad54Feb51A28" + } + ]` + + // Unmarshal the JSON response + var holdings []models.VoucherHolding + err := json.Unmarshal([]byte(mockJSON), &holdings) + if err != nil { + return nil, err + } + + return holdings, nil +} diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 165a456..19c7d1a 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -231,36 +231,23 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt return res, nil } +// GetVoucherList fetches the list of vouchers and formats them +// checks whether they are synced internally before calling the API func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - vouchers := []string{ - "SRF", - "CRF", - "VCF", - "VSAPA", - "FSTMP", - "FSAW", - "PTAQ", - "VCRXT", - "VSGAQ", - "QPWIQQ", - "FSTMP", - "FSAW", - "PTAQ", - "VCRXT", - "VSGAQ", - "QPWIQQ", - "FSTMP", - "FSAW", - "PTAQ", - "VCRXT", - "VSGAQ", - "QPWIQQ", + + // check if the vouchers exist internally and if not + // fetch from the API + + // Fetch vouchers from API + vouchers, err := h.accountService.FetchVouchersFromAPI() + if err != nil { + return res, fmt.Errorf("error fetching vouchers: %w", err) } var numberedVouchers []string for i, voucher := range vouchers { - numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher)) + numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.Symbol)) } res.Content = strings.Join(numberedVouchers, "\n") diff --git a/internal/mocks/servicemock.go b/internal/mocks/servicemock.go index 9fb6d3e..30386b3 100644 --- a/internal/mocks/servicemock.go +++ b/internal/mocks/servicemock.go @@ -23,4 +23,9 @@ func (m *MockAccountService) CheckBalance(publicKey string) (string, error) { func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, error) { args := m.Called(trackingId) return args.String(0), args.Error(1) -} \ No newline at end of file +} + +func (m *MockAccountService) FetchVouchersFromAPI() ([]models.VoucherHolding, error) { + args := m.Called() + return args.Get(0).([]models.VoucherHolding), args.Error(1) +} diff --git a/internal/models/vouchersresponse.go b/internal/models/vouchersresponse.go new file mode 100644 index 0000000..08967b7 --- /dev/null +++ b/internal/models/vouchersresponse.go @@ -0,0 +1,7 @@ +package models + +// VoucherHolding represents a single voucher holding +type VoucherHolding struct { + Symbol string `json:"symbol"` + Address string `json:"address"` +} \ No newline at end of file From 517f9806643b4012cf6aaedfbe3450f4c781c0c6 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Sat, 5 Oct 2024 16:56:49 +0300 Subject: [PATCH 30/34] get the vouchers list and store in gdbm --- internal/handlers/handlerservice.go | 1 + internal/handlers/server/accountservice.go | 100 +++++++++++++++------ internal/handlers/ussd/menuhandler.go | 63 +++++++++++-- internal/mocks/servicemock.go | 4 +- internal/models/vouchersresponse.go | 18 ++-- internal/utils/db.go | 1 + services/registration/main.vis | 2 + 7 files changed, 152 insertions(+), 37 deletions(-) diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 1a556cc..8f0ff16 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -92,6 +92,7 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { ls.DbRs.AddLocalFunc("verify_new_pin", ussdHandlers.VerifyNewPin) ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange) ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) + ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) return ussdHandlers, nil diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go index ca460c7..d53db57 100644 --- a/internal/handlers/server/accountservice.go +++ b/internal/handlers/server/accountservice.go @@ -13,14 +13,12 @@ type AccountServiceInterface interface { CheckBalance(publicKey string) (string, error) CreateAccount() (*models.AccountResponse, error) CheckAccountStatus(trackingId string) (string, error) - FetchVouchersFromAPI() ([]models.VoucherHolding, error) + FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) } type AccountService struct { } - - // CheckAccountStatus retrieves the status of an account transaction based on the provided tracking ID. // // Parameters: @@ -28,12 +26,10 @@ type AccountService struct { // CreateAccount or a similar function that returns an AccountResponse. The `trackingId` field in the // AccountResponse struct can be used here to check the account status during a transaction. // -// // Returns: // - string: The status of the transaction as a string. If there is an error during the request or processing, this will be an empty string. // - error: An error if any occurred during the HTTP request, reading the response, or unmarshalling the JSON data. // If no error occurs, this will be nil. -// func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) { resp, err := http.Get(config.TrackStatusURL + trackingId) if err != nil { @@ -57,7 +53,6 @@ func (as *AccountService) CheckAccountStatus(trackingId string) (string, error) return status, nil } - // CheckBalance retrieves the balance for a given public key from the custodial balance API endpoint. // Parameters: // - publicKey: The public key associated with the account whose balance needs to be checked. @@ -84,8 +79,7 @@ func (as *AccountService) CheckBalance(publicKey string) (string, error) { return balance, nil } - -//CreateAccount creates a new account in the custodial system. +// CreateAccount creates a new account in the custodial system. // Returns: // - *models.AccountResponse: A pointer to an AccountResponse struct containing the details of the created account. // If there is an error during the request or processing, this will be nil. @@ -112,30 +106,86 @@ func (as *AccountService) CreateAccount() (*models.AccountResponse, error) { return &accountResp, nil } -// fetchVouchersFromAPI calls the API to get the list of vouchers belonging to the user -func (as *AccountService) FetchVouchersFromAPI() ([]models.VoucherHolding, error) { +// FetchVouchers retrieves the token holdings for a given public key from the custodial holdings API endpoint +// Parameters: +// - publicKey: The public key associated with the account. +func (as *AccountService) FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) { // TODO replace with the actual request once ready - mockJSON := `[ - { - "symbol": "MUMO", - "address": "0x078b3a26596218507781722A4e8825BFB9570Fba" - }, - { - "symbol": "SRF", - "address": "0x45d747172e77d55575c197CbA9451bC2CD8F4958" - }, - { - "symbol": "HALGAN", - "address": "0x12169Fb5931A599ad1283bb8311Dad54Feb51A28" + mockJSON := `{ + "ok": true, + "description": "Token holdings with current balances", + "result": { + "holdings": [ + { + "contractAddress": "0x6CC75A06ac72eB4Db2eE22F781F5D100d8ec03ee", + "tokenSymbol": "FSPTST", + "tokenDecimals": "6", + "balance": "8869964242" + }, + { + "contractAddress": "0x724F2910D790B54A39a7638282a45B1D83564fFA", + "tokenSymbol": "GEO", + "tokenDecimals": "6", + "balance": "9884" + }, + { + "contractAddress": "0x2105a206B7bec31E2F90acF7385cc8F7F5f9D273", + "tokenSymbol": "MFNK", + "tokenDecimals": "6", + "balance": "19788697" + }, + { + "contractAddress": "0x63DE2Ac8D1008351Cc69Fb8aCb94Ba47728a7E83", + "tokenSymbol": "MILO", + "tokenDecimals": "6", + "balance": "75" + }, + { + "contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", + "tokenSymbol": "SOHAIL", + "tokenDecimals": "6", + "balance": "27874115" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SRQIF", + "tokenDecimals": "6", + "balance": "2745987" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SRFI", + "tokenDecimals": "6", + "balance": "2745987" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SRFU", + "tokenDecimals": "6", + "balance": "2745987" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SRQF", + "tokenDecimals": "6", + "balance": "2745987" + }, + { + "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", + "tokenSymbol": "SREF", + "tokenDecimals": "6", + "balance": "2745987" + } + ] } - ]` + }` // Unmarshal the JSON response - var holdings []models.VoucherHolding + var holdings models.VoucherHoldingResponse err := json.Unmarshal([]byte(mockJSON), &holdings) if err != nil { return nil, err } - return holdings, nil + return &holdings, nil } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 19c7d1a..d55abaf 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -3,6 +3,7 @@ package ussd import ( "bytes" "context" + "encoding/json" "fmt" "path" "regexp" @@ -232,22 +233,36 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt } // GetVoucherList fetches the list of vouchers and formats them -// checks whether they are synced internally before calling the API +// checks whether they are stored internally before calling the API func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } // check if the vouchers exist internally and if not // fetch from the API - // Fetch vouchers from API - vouchers, err := h.accountService.FetchVouchersFromAPI() + // Read vouchers from the store + store := h.userdataStore + voucherData, err := store.ReadEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST) if err != nil { - return res, fmt.Errorf("error fetching vouchers: %w", err) + return res, err + } + + // Unmarshal the stored JSON data into the correct struct + var vouchers []struct { + TokenSymbol string `json:"tokenSymbol"` + } + err = json.Unmarshal(voucherData, &vouchers) + if err != nil { + return res, fmt.Errorf("failed to unmarshal vouchers: %v", err) } var numberedVouchers []string for i, voucher := range vouchers { - numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.Symbol)) + numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol)) } res.Content = strings.Join(numberedVouchers, "\n") @@ -1007,3 +1022,41 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) return res, nil } + +// CheckVouchers retrieves the token holdings from the API using the "PublicKey" and stores +// them to gdbm +func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + + sessionId, ok := ctx.Value("SessionId").(string) + if !ok { + return res, fmt.Errorf("missing session") + } + + store := h.userdataStore + publicKey, err := store.ReadEntry(ctx, sessionId, utils.DATA_PUBLIC_KEY) + if err != nil { + return res, nil + } + + // Fetch vouchers from the API + vouchersResp, err := h.accountService.FetchVouchers(string(publicKey)) + if err != nil { + return res, nil + } + + // Convert only the list of holdings (vouchers) to JSON + voucherBytes, err := json.Marshal(vouchersResp.Result.Holdings) + if err != nil { + return res, nil + } + + // Store the voucher symbols in the userdataStore + err = store.WriteEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST, voucherBytes) + if err != nil { + return res, nil + } + + return res, nil +} diff --git a/internal/mocks/servicemock.go b/internal/mocks/servicemock.go index 30386b3..8fbde0f 100644 --- a/internal/mocks/servicemock.go +++ b/internal/mocks/servicemock.go @@ -25,7 +25,7 @@ func (m *MockAccountService) CheckAccountStatus(trackingId string) (string, erro return args.String(0), args.Error(1) } -func (m *MockAccountService) FetchVouchersFromAPI() ([]models.VoucherHolding, error) { +func (m *MockAccountService) FetchVouchers(publicKey string) (*models.VoucherHoldingResponse, error) { args := m.Called() - return args.Get(0).([]models.VoucherHolding), args.Error(1) + return args.Get(0).(*models.VoucherHoldingResponse), args.Error(1) } diff --git a/internal/models/vouchersresponse.go b/internal/models/vouchersresponse.go index 08967b7..010730f 100644 --- a/internal/models/vouchersresponse.go +++ b/internal/models/vouchersresponse.go @@ -1,7 +1,15 @@ package models -// VoucherHolding represents a single voucher holding -type VoucherHolding struct { - Symbol string `json:"symbol"` - Address string `json:"address"` -} \ No newline at end of file +// VoucherHoldingResponse represents a single voucher holding +type VoucherHoldingResponse struct { + Ok bool `json:"ok"` + Description string `json:"description"` + Result struct { + Holdings []struct { + ContractAddress string `json:"contractAddress"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimals string `json:"tokenDecimals"` + Balance string `json:"balance"` + } `json:"holdings"` + } `json:"result"` +} diff --git a/internal/utils/db.go b/internal/utils/db.go index 410da68..3080c1b 100644 --- a/internal/utils/db.go +++ b/internal/utils/db.go @@ -23,6 +23,7 @@ const ( DATA_RECIPIENT DATA_AMOUNT DATA_TEMPORARY_PIN + DATA_VOUCHER_LIST ) func typToBytes(typ DataTyp) []byte { diff --git a/services/registration/main.vis b/services/registration/main.vis index 1009039..88f8a42 100644 --- a/services/registration/main.vis +++ b/services/registration/main.vis @@ -1,5 +1,7 @@ LOAD check_balance 64 RELOAD check_balance +LOAD check_vouchers 10 +RELOAD check_vouchers MAP check_balance MOUT send 1 MOUT vouchers 2 From 755899be4e6a1e0dc5c6b731910ade18d40284ad Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 7 Oct 2024 08:59:15 +0300 Subject: [PATCH 31/34] store the pre-rendered vouchers list --- internal/handlers/ussd/menuhandler.go | 32 +++++++-------------------- internal/storage/db.go | 28 +++++++++++------------ 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index d55abaf..4c09acb 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -3,7 +3,6 @@ package ussd import ( "bytes" "context" - "encoding/json" "fmt" "path" "regexp" @@ -251,20 +250,7 @@ func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) return res, err } - // Unmarshal the stored JSON data into the correct struct - var vouchers []struct { - TokenSymbol string `json:"tokenSymbol"` - } - err = json.Unmarshal(voucherData, &vouchers) - if err != nil { - return res, fmt.Errorf("failed to unmarshal vouchers: %v", err) - } - - var numberedVouchers []string - for i, voucher := range vouchers { - numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol)) - } - res.Content = strings.Join(numberedVouchers, "\n") + res.Content = string(voucherData) return res, nil } @@ -1027,8 +1013,6 @@ func (h *Handlers) GetProfileInfo(ctx context.Context, sym string, input []byte) // them to gdbm func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - var err error - sessionId, ok := ctx.Value("SessionId").(string) if !ok { return res, fmt.Errorf("missing session") @@ -1040,20 +1024,20 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) return res, nil } - // Fetch vouchers from the API + // Fetch vouchers from the API using the public key vouchersResp, err := h.accountService.FetchVouchers(string(publicKey)) if err != nil { return res, nil } - // Convert only the list of holdings (vouchers) to JSON - voucherBytes, err := json.Marshal(vouchersResp.Result.Holdings) - if err != nil { - return res, nil + var numberedVouchers []string + for i, voucher := range vouchersResp.Result.Holdings { + numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol)) } - // Store the voucher symbols in the userdataStore - err = store.WriteEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST, voucherBytes) + voucherList := strings.Join(numberedVouchers, "\n") + + err = store.WriteEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST, []byte(voucherList)) if err != nil { return res, nil } diff --git a/internal/storage/db.go b/internal/storage/db.go index b2ac6a9..060f0c0 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -12,32 +12,32 @@ const ( type SubPrefixDb struct { store db.Db - pfx []byte + pfx []byte } func NewSubPrefixDb(store db.Db, pfx []byte) *SubPrefixDb { return &SubPrefixDb{ store: store, - pfx: pfx, + pfx: pfx, } } -func(s *SubPrefixDb) toKey(k []byte) []byte { - return append(s.pfx, k...) +func (s *SubPrefixDb) toKey(k []byte) []byte { + return append(s.pfx, k...) } -func(s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { - s.store.SetPrefix(DATATYPE_USERSUB) +func (s *SubPrefixDb) Get(ctx context.Context, key []byte) ([]byte, error) { + s.store.SetPrefix(DATATYPE_USERSUB) key = s.toKey(key) - v, err := s.store.Get(ctx, key) - if err != nil { - return nil, err - } - return v, nil + v, err := s.store.Get(ctx, key) + if err != nil { + return nil, err + } + return v, nil } -func(s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { - s.store.SetPrefix(DATATYPE_USERSUB) +func (s *SubPrefixDb) Put(ctx context.Context, key []byte, val []byte) error { + s.store.SetPrefix(DATATYPE_USERSUB) key = s.toKey(key) - return s.store.Put(ctx, key, val) + return s.store.Put(ctx, key, val) } From e4ed9a65bbc777bf98232a0b887fab73530ae291 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 7 Oct 2024 13:49:12 +0300 Subject: [PATCH 32/34] store the symbols and balances --- internal/handlers/server/accountservice.go | 36 ---------------------- internal/handlers/ussd/menuhandler.go | 29 +++++++++++------ 2 files changed, 19 insertions(+), 46 deletions(-) diff --git a/internal/handlers/server/accountservice.go b/internal/handlers/server/accountservice.go index d53db57..9f6ce03 100644 --- a/internal/handlers/server/accountservice.go +++ b/internal/handlers/server/accountservice.go @@ -139,42 +139,6 @@ func (as *AccountService) FetchVouchers(publicKey string) (*models.VoucherHoldin "tokenSymbol": "MILO", "tokenDecimals": "6", "balance": "75" - }, - { - "contractAddress": "0xd4c288865Ce0985a481Eef3be02443dF5E2e4Ea9", - "tokenSymbol": "SOHAIL", - "tokenDecimals": "6", - "balance": "27874115" - }, - { - "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", - "tokenSymbol": "SRQIF", - "tokenDecimals": "6", - "balance": "2745987" - }, - { - "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", - "tokenSymbol": "SRFI", - "tokenDecimals": "6", - "balance": "2745987" - }, - { - "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", - "tokenSymbol": "SRFU", - "tokenDecimals": "6", - "balance": "2745987" - }, - { - "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", - "tokenSymbol": "SRQF", - "tokenDecimals": "6", - "balance": "2745987" - }, - { - "contractAddress": "0x45d747172e77d55575c197CbA9451bC2CD8F4958", - "tokenSymbol": "SREF", - "tokenDecimals": "6", - "balance": "2745987" } ] } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 4c09acb..0ff1017 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -21,6 +21,8 @@ import ( "git.grassecon.net/urdt/ussd/internal/handlers/server" "git.grassecon.net/urdt/ussd/internal/utils" "gopkg.in/leonelquinteros/gotext.v1" + + "git.grassecon.net/urdt/ussd/internal/storage" ) var ( @@ -235,19 +237,17 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt // checks whether they are stored internally before calling the API func (h *Handlers) GetVoucherList(ctx context.Context, sym string, input []byte) (resource.Result, error) { var res resource.Result - sessionId, ok := ctx.Value("SessionId").(string) - if !ok { - return res, fmt.Errorf("missing session") - } // check if the vouchers exist internally and if not // fetch from the API // Read vouchers from the store store := h.userdataStore - voucherData, err := store.ReadEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST) + prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings")) + + voucherData, err := prefixdb.Get(ctx, []byte("tokens")) if err != nil { - return res, err + return res, nil } res.Content = string(voucherData) @@ -1030,14 +1030,23 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) return res, nil } - var numberedVouchers []string + var numberedSymbols []string + var numberedBalances []string for i, voucher := range vouchersResp.Result.Holdings { - numberedVouchers = append(numberedVouchers, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol)) + numberedSymbols = append(numberedSymbols, fmt.Sprintf("%d:%s", i+1, voucher.TokenSymbol)) + numberedBalances = append(numberedBalances, fmt.Sprintf("%d:%s", i+1, voucher.Balance)) } - voucherList := strings.Join(numberedVouchers, "\n") + voucherSymbolList := strings.Join(numberedSymbols, "\n") + voucherBalanceList := strings.Join(numberedBalances, "\n") - err = store.WriteEntry(ctx, sessionId, utils.DATA_VOUCHER_LIST, []byte(voucherList)) + prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings")) + err = prefixdb.Put(ctx, []byte("tokens"), []byte(voucherSymbolList)) + if err != nil { + return res, nil + } + + err = prefixdb.Put(ctx, []byte(voucherSymbolList), []byte(voucherBalanceList)) if err != nil { return res, nil } From 06ebcc0f07886f05ece7c0c04e3a71ad6312c5e5 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 7 Oct 2024 16:15:13 +0300 Subject: [PATCH 33/34] added swahili template --- services/registration/select_voucher_swa | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 services/registration/select_voucher_swa diff --git a/services/registration/select_voucher_swa b/services/registration/select_voucher_swa new file mode 100644 index 0000000..b4720bf --- /dev/null +++ b/services/registration/select_voucher_swa @@ -0,0 +1,2 @@ +Chagua nambari au ishara kutoka kwa salio zako: +{{.get_vouchers}} \ No newline at end of file From cb4a52e4f245bddf9755434004a4cdb7fffd9f65 Mon Sep 17 00:00:00 2001 From: alfred-mk Date: Mon, 7 Oct 2024 16:16:57 +0300 Subject: [PATCH 34/34] view a selected voucher and verify the PIN --- internal/handlers/handlerservice.go | 1 + internal/handlers/ussd/menuhandler.go | 68 ++++++++++++++++++++++++ services/registration/my_vouchers.vis | 3 -- services/registration/pp.csv | 2 +- services/registration/select_voucher.vis | 6 ++- services/registration/view_voucher | 2 + services/registration/view_voucher.vis | 11 ++++ services/registration/view_voucher_swa | 2 + services/registration/voucher_set | 1 + services/registration/voucher_set.vis | 5 ++ services/registration/voucher_set_swa | 1 + 11 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 services/registration/view_voucher create mode 100644 services/registration/view_voucher.vis create mode 100644 services/registration/view_voucher_swa create mode 100644 services/registration/voucher_set create mode 100644 services/registration/voucher_set.vis create mode 100644 services/registration/voucher_set_swa diff --git a/internal/handlers/handlerservice.go b/internal/handlers/handlerservice.go index 8f0ff16..4c152b9 100644 --- a/internal/handlers/handlerservice.go +++ b/internal/handlers/handlerservice.go @@ -94,6 +94,7 @@ func (ls *LocalHandlerService) GetHandler() (*ussd.Handlers, error) { ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp) ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers) ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList) + ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher) return ussdHandlers, nil } diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 0ff1017..c254401 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -1053,3 +1053,71 @@ func (h *Handlers) CheckVouchers(ctx context.Context, sym string, input []byte) return res, nil } + +// ViewVoucher retrieves the token holding and balance from the subprefixDB +func (h *Handlers) ViewVoucher(ctx context.Context, sym string, input []byte) (resource.Result, error) { + var res resource.Result + var err error + inputStr := string(input) + + if inputStr == "0" || inputStr == "00" { + return res, nil + } + + flag_incorrect_voucher, _ := h.flagManager.GetFlag("flag_incorrect_voucher") + + // Initialize the store and prefix database + store := h.userdataStore + prefixdb := storage.NewSubPrefixDb(store, []byte("token_holdings")) + + // Retrieve the voucher symbol list + voucherSymbolList, err := prefixdb.Get(ctx, []byte("tokens")) + if err != nil { + return res, fmt.Errorf("failed to retrieve voucher symbol list: %v", err) + } + + // Retrieve the voucher balance list + voucherBalanceList, err := prefixdb.Get(ctx, []byte(voucherSymbolList)) + if err != nil { + return res, fmt.Errorf("failed to retrieve voucher balance list: %v", err) + } + + // Convert the symbol and balance lists from byte arrays to strings + voucherSymbols := string(voucherSymbolList) + voucherBalances := string(voucherBalanceList) + + // Split the lists into slices for processing + symbols := strings.Split(voucherSymbols, "\n") + balances := strings.Split(voucherBalances, "\n") + + var matchedSymbol, matchedBalance string + + for i, symbol := range symbols { + symbolParts := strings.SplitN(symbol, ":", 2) + if len(symbolParts) != 2 { + continue + } + voucherNum := symbolParts[0] + voucherSymbol := symbolParts[1] + + // Check if input matches either the number or the symbol + if inputStr == voucherNum || strings.EqualFold(inputStr, voucherSymbol) { + matchedSymbol = voucherSymbol + // Ensure there's a corresponding balance + if i < len(balances) { + matchedBalance = strings.SplitN(balances[i], ":", 2)[1] // Extract balance after the "x:balance" format + } + break + } + } + + // If a match is found, return the symbol and balance + if matchedSymbol != "" && matchedBalance != "" { + res.Content = fmt.Sprintf("%s\n%s", matchedSymbol, matchedBalance) + res.FlagReset = append(res.FlagReset, flag_incorrect_voucher) + } else { + res.FlagSet = append(res.FlagSet, flag_incorrect_voucher) + } + + return res, nil +} diff --git a/services/registration/my_vouchers.vis b/services/registration/my_vouchers.vis index b70dbfa..9702573 100644 --- a/services/registration/my_vouchers.vis +++ b/services/registration/my_vouchers.vis @@ -4,6 +4,3 @@ MOUT back 0 HALT INCMP _ 0 INCMP select_voucher 1 - - - diff --git a/services/registration/pp.csv b/services/registration/pp.csv index fd552e4..2a7bb21 100644 --- a/services/registration/pp.csv +++ b/services/registration/pp.csv @@ -12,5 +12,5 @@ flag,flag_invalid_amount,18,this is set when the given transaction amount is inv flag,flag_incorrect_pin,19,this is set when the provided PIN is invalid or does not match the current account's PIN flag,flag_valid_pin,20,this is set when the given PIN is valid flag,flag_allow_update,21,this is set to allow a user to update their profile data -flag,flag_single_edit,22,this is set to allow a user to edit a single profile item such as year of birth +flag,flag_incorrect_voucher,22,this is set when the selected voucher is invalid flag,flag_incorrect_date_format,23,this is set when the given year of birth is invalid diff --git a/services/registration/select_voucher.vis b/services/registration/select_voucher.vis index 4fb1d40..50b99ad 100644 --- a/services/registration/select_voucher.vis +++ b/services/registration/select_voucher.vis @@ -1,11 +1,15 @@ LOAD get_vouchers 0 MAP get_vouchers MOUT back 0 -MOUT quit 9 +MOUT quit 00 MNEXT next 11 MPREV prev 22 HALT +LOAD view_voucher 80 +RELOAD view_voucher +CATCH . flag_incorrect_voucher 1 INCMP _ 0 INCMP quit 9 INCMP > 11 INCMP < 22 +INCMP view_voucher * diff --git a/services/registration/view_voucher b/services/registration/view_voucher new file mode 100644 index 0000000..3940982 --- /dev/null +++ b/services/registration/view_voucher @@ -0,0 +1,2 @@ +Enter PIN to confirm selection: +{{.view_voucher}} \ No newline at end of file diff --git a/services/registration/view_voucher.vis b/services/registration/view_voucher.vis new file mode 100644 index 0000000..ee8bf85 --- /dev/null +++ b/services/registration/view_voucher.vis @@ -0,0 +1,11 @@ +RELOAD view_voucher +MAP view_voucher +MOUT back 0 +MOUT quit 9 +LOAD authorize_account 6 +HALT +RELOAD authorize_account +CATCH incorrect_pin flag_incorrect_pin 1 +INCMP _ 0 +INCMP quit 9 +INCMP voucher_set * diff --git a/services/registration/view_voucher_swa b/services/registration/view_voucher_swa new file mode 100644 index 0000000..485e2ef --- /dev/null +++ b/services/registration/view_voucher_swa @@ -0,0 +1,2 @@ +Weka PIN ili kuthibitisha chaguo: +{{.view_voucher}} \ No newline at end of file diff --git a/services/registration/voucher_set b/services/registration/voucher_set new file mode 100644 index 0000000..838c1ef --- /dev/null +++ b/services/registration/voucher_set @@ -0,0 +1 @@ +Success! symbol is now your active voucher. \ No newline at end of file diff --git a/services/registration/voucher_set.vis b/services/registration/voucher_set.vis new file mode 100644 index 0000000..832ef22 --- /dev/null +++ b/services/registration/voucher_set.vis @@ -0,0 +1,5 @@ +MOUT back 0 +MOUT quit 9 +HALT +INCMP ^ 0 +INCMP quit 9 diff --git a/services/registration/voucher_set_swa b/services/registration/voucher_set_swa new file mode 100644 index 0000000..320a315 --- /dev/null +++ b/services/registration/voucher_set_swa @@ -0,0 +1 @@ +Hongera! symbol ni Sarafu inayotumika sasa. \ No newline at end of file