diff --git a/cmd/http/main.go b/cmd/http/main.go new file mode 100644 index 0000000..7b085a8 --- /dev/null +++ b/cmd/http/main.go @@ -0,0 +1,215 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/http" + "os" + "os/signal" + "path" + "strconv" + "syscall" + + "git.defalsify.org/vise.git/asm" + "git.defalsify.org/vise.git/db" + fsdb "git.defalsify.org/vise.git/db/fs" + gdbmdb "git.defalsify.org/vise.git/db/gdbm" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/resource" + "git.defalsify.org/vise.git/logging" + + "git.grassecon.net/urdt/ussd/internal/handlers/ussd" + httpserver "git.grassecon.net/urdt/ussd/internal/http" +) + +var ( + logg = logging.NewVanilla() + scriptDir = path.Join("services", "registration") +) + +func getFlags(fp string, debug bool) (*asm.FlagParser, error) { + flagParser := asm.NewFlagParser().WithDebug() + _, err := flagParser.Load(fp) + if err != nil { + return nil, err + } + return flagParser, nil +} + +func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, userdataStore db.Db) (*ussd.Handlers, error) { + + ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) + if err != nil { + return nil, err + } + rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage) + rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) + rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) + rs.AddLocalFunc("verify_pin", ussdHandlers.VerifyPin) + rs.AddLocalFunc("check_identifier", ussdHandlers.CheckIdentifier) + rs.AddLocalFunc("check_account_status", ussdHandlers.CheckAccountStatus) + rs.AddLocalFunc("authorize_account", ussdHandlers.Authorize) + rs.AddLocalFunc("quit", ussdHandlers.Quit) + rs.AddLocalFunc("check_balance", ussdHandlers.CheckBalance) + rs.AddLocalFunc("validate_recipient", ussdHandlers.ValidateRecipient) + rs.AddLocalFunc("transaction_reset", ussdHandlers.TransactionReset) + rs.AddLocalFunc("max_amount", ussdHandlers.MaxAmount) + rs.AddLocalFunc("validate_amount", ussdHandlers.ValidateAmount) + rs.AddLocalFunc("reset_transaction_amount", ussdHandlers.ResetTransactionAmount) + rs.AddLocalFunc("get_recipient", ussdHandlers.GetRecipient) + rs.AddLocalFunc("get_sender", ussdHandlers.GetSender) + rs.AddLocalFunc("get_amount", ussdHandlers.GetAmount) + rs.AddLocalFunc("reset_incorrect", ussdHandlers.ResetIncorrectPin) + rs.AddLocalFunc("save_firstname", ussdHandlers.SaveFirstname) + rs.AddLocalFunc("save_familyname", ussdHandlers.SaveFamilyname) + rs.AddLocalFunc("save_gender", ussdHandlers.SaveGender) + rs.AddLocalFunc("save_location", ussdHandlers.SaveLocation) + rs.AddLocalFunc("save_yob", ussdHandlers.SaveYob) + rs.AddLocalFunc("save_offerings", ussdHandlers.SaveOfferings) + rs.AddLocalFunc("quit_with_balance", ussdHandlers.QuitWithBalance) + rs.AddLocalFunc("reset_account_authorized", ussdHandlers.ResetAccountAuthorized) + rs.AddLocalFunc("reset_allow_update", ussdHandlers.ResetAllowUpdate) + rs.AddLocalFunc("get_profile_info", ussdHandlers.GetProfileInfo) + rs.AddLocalFunc("verify_yob", ussdHandlers.VerifyYob) + rs.AddLocalFunc("reset_incorrect_date_format", ussdHandlers.ResetIncorrectYob) + rs.AddLocalFunc("set_reset_single_edit", ussdHandlers.SetResetSingleEdit) + rs.AddLocalFunc("initiate_transaction", ussdHandlers.InitiateTransaction) + + return ussdHandlers, nil +} + +func ensureDbDir(dbDir string) error { + err := os.MkdirAll(dbDir, 0700) + if err != nil { + return fmt.Errorf("state dir create exited with error: %v\n", err) + } + return nil +} + +func getStateStore(dbDir string, ctx context.Context) (db.Db, error) { + store := gdbmdb.NewGdbmDb() + storeFile := path.Join(dbDir, "state.gdbm") + store.Connect(ctx, storeFile) + return store, nil +} + +func getUserdataDb(dbDir string, ctx context.Context) db.Db { + store := gdbmdb.NewGdbmDb() + storeFile := path.Join(dbDir, "userdata.gdbm") + store.Connect(ctx, storeFile) + + return store +} + +func getResource(resourceDir string, ctx context.Context) (resource.Resource, error) { + store := fsdb.NewFsDb() + err := store.Connect(ctx, resourceDir) + if err != nil { + return nil, err + } + rfs := resource.NewDbResource(store) + return rfs, nil +} + +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", 7123, "http port") + flag.Parse() + + logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size) + + ctx := context.Background() + pfp := path.Join(scriptDir, "pp.csv") + flagParser, err := getFlags(pfp, true) + + if err != nil { + os.Exit(1) + } + + cfg := engine.Config{ + Root: "root", + OutputSize: uint32(size), + FlagCount: uint32(16), + } + if stateDebug { + cfg.StateDebug = true + } + if engineDebug { + cfg.EngineDebug = true + } + + rs, err := getResource(resourceDir, ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + err = ensureDbDir(dbDir) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + userdataStore := getUserdataDb(dbDir, ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + defer userdataStore.Close() + + dbResource, ok := rs.(*resource.DbResource) + if !ok { + os.Exit(1) + } + + hl, err := getHandler(flagParser, dbResource, userdataStore) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + stateStore, err := getStateStore(dbDir, ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + defer stateStore.Close() + + rp := &httpserver.DefaultRequestParser{} + //sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl.Init) + sh := httpserver.NewSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl) + s := &http.Server{ + Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), + Handler: sh, + } + s.RegisterOnShutdown(sh.Shutdown) + + cint := make(chan os.Signal) + cterm := make(chan os.Signal) + signal.Notify(cint, os.Interrupt, syscall.SIGINT) + signal.Notify(cterm, os.Interrupt, syscall.SIGTERM) + go func() { + select { + case _ = <-cint: + case _ = <-cterm: + } + s.Shutdown(ctx) + }() + err = s.ListenAndServe() + if err != nil { + logg.Infof("Server closed with error", "err", err) + } +} diff --git a/cmd/main.go b/cmd/main.go index fc734d4..9222c13 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,10 +34,11 @@ func getParser(fp string, debug bool) (*asm.FlagParser, error) { func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.Persister, userdataStore db.Db) (*ussd.Handlers, error) { - ussdHandlers, err := ussd.NewHandlers(appFlags, pe, userdataStore) + ussdHandlers, err := ussd.NewHandlers(appFlags, userdataStore) if err != nil { return nil, err } + ussdHandlers = ussdHandlers.WithPersister(pe) rs.AddLocalFunc("select_language", ussdHandlers.SetLanguage) rs.AddLocalFunc("create_account", ussdHandlers.CreateAccount) rs.AddLocalFunc("save_pin", ussdHandlers.SavePin) @@ -74,10 +75,18 @@ func getHandler(appFlags *asm.FlagParser, rs *resource.DbResource, pe *persist.P return ussdHandlers, nil } -func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) { +func ensureDbDir(dbDir string) error { err := os.MkdirAll(dbDir, 0700) if err != nil { - return nil, fmt.Errorf("state dir create exited with error: %v\n", err) + return fmt.Errorf("state dir create exited with error: %v\n", err) + } + return nil +} + +func getPersister(dbDir string, ctx context.Context) (*persist.Persister, error) { + err := ensureDbDir(dbDir) + if err != nil { + return nil, err } store := gdbmdb.NewGdbmDb() storeFile := path.Join(dbDir, "state.gdbm") @@ -145,7 +154,7 @@ func main() { os.Exit(1) } - pr, err := getPersister(dbDir, ctx) + pe, err := getPersister(dbDir, ctx) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) @@ -159,16 +168,17 @@ func main() { dbResource, ok := rs.(*resource.DbResource) if !ok { + fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - hl, err := getHandler(flagParser, dbResource, pr, store) + hl, err := getHandler(flagParser, dbResource, pe, store) if err != nil { fmt.Fprintf(os.Stderr, err.Error()) os.Exit(1) } - en := getEngine(cfg, rs, pr) + en := getEngine(cfg, rs, pe) en = en.WithFirst(hl.Init) if debug { en = en.WithDebug(nil) diff --git a/go.mod b/go.mod index 6ee0aec..71730c4 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,12 @@ module git.grassecon.net/urdt/ussd go 1.22.6 +require ( + git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7 + github.com/alecthomas/assert/v2 v2.2.2 + gopkg.in/leonelquinteros/gotext.v1 v1.3.1 +) + require ( github.com/alecthomas/participle/v2 v2.0.0 // indirect github.com/alecthomas/repr v0.2.0 // indirect @@ -17,10 +23,3 @@ require ( github.com/x448/float16 v0.8.4 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -require ( - git.defalsify.org/vise.git v0.1.0-rc.1.0.20240906020635-400f69d01a89 - 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 2624d07..b40a422 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -git.defalsify.org/vise.git v0.1.0-rc.1.0.20240906020635-400f69d01a89 h1:YyQODhMwSM5YD9yKHM5jCF0HC0RQtE3MkVXcTnOhXJo= -git.defalsify.org/vise.git v0.1.0-rc.1.0.20240906020635-400f69d01a89/go.mod h1:JDguWmcoWBdsnpw7PUjVZAEpdC/ubBmjdUBy3tjP63M= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7 h1:embPZDx0Sgpq6jp9vcZ1GVI0eum3PsPCmAfxAa/1KLI= +git.defalsify.org/vise.git v0.1.0-rc.3.0.20240911162138-1f2af8672dc7/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= diff --git a/internal/handlers/ussd/menuhandler.go b/internal/handlers/ussd/menuhandler.go index 64b6a14..6773421 100644 --- a/internal/handlers/ussd/menuhandler.go +++ b/internal/handlers/ussd/menuhandler.go @@ -66,21 +66,17 @@ type Handlers struct { accountService server.AccountServiceInterface } -func NewHandlers(parser *asm.FlagParser, pe *persist.Persister, userdataStore db.Db) (*Handlers, error) { - userDb := utils.UserDataStore{ - Db: userdataStore, - } - if pe == nil { - return nil, fmt.Errorf("cannot create handler with nil persister") - } +func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db) (*Handlers, error) { if userdataStore == nil { return nil, fmt.Errorf("cannot create handler with nil userdata store") } + userDb := &utils.UserDataStore{ + Db: userdataStore, + } h := &Handlers{ - pe: pe, - userdataStore: &userDb, - flagManager: parser, - accountService: &server.AccountService{}, + userdataStore: userDb, + flagManager: appFlags, + accountService: &server.AccountService{}, } return h, nil } @@ -94,6 +90,14 @@ func isValidPIN(pin string) bool { return match } +func (h *Handlers) WithPersister(pe *persist.Persister) *Handlers { + if h.pe != nil { + panic("persister already set") + } + h.pe = pe + return h +} + func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource.Result, error) { var r resource.Result diff --git a/internal/http/server.go b/internal/http/server.go new file mode 100644 index 0000000..7d1d8fe --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,150 @@ +package http + +import ( + "fmt" + "io/ioutil" + "net/http" + + "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/engine" + "git.defalsify.org/vise.git/logging" + "git.defalsify.org/vise.git/persist" + "git.defalsify.org/vise.git/resource" + + "git.grassecon.net/urdt/ussd/internal/handlers/ussd" +) + +var ( + logg = logging.NewVanilla().WithDomain("httpserver") +) + +type RequestParser interface { + GetSessionId(rq *http.Request) (string, error) + GetInput(rq *http.Request) ([]byte, error) +} + +type DefaultRequestParser struct { +} + +func(rp *DefaultRequestParser) GetSessionId(rq *http.Request) (string, error) { + v := rq.Header.Get("X-Vise-Session") + if v == "" { + return "", fmt.Errorf("no session found") + } + return v, nil +} + +func(rp *DefaultRequestParser) GetInput(rq *http.Request) ([]byte, error) { + defer rq.Body.Close() + v, err := ioutil.ReadAll(rq.Body) + if err != nil { + return nil, err + } + return v, nil +} + +type SessionHandler struct { + cfgTemplate engine.Config + rp RequestParser + rs resource.Resource + //first resource.EntryFunc + hn *ussd.Handlers + provider StorageProvider +} + +//func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, first resource.EntryFunc) *SessionHandler { +func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *SessionHandler { + return &SessionHandler{ + cfgTemplate: cfg, + rs: rs, + //first: first, + hn: hn, + rp: rp, + provider: NewSimpleStorageProvider(stateDb, userdataDb), + } +} + +func(f *SessionHandler) writeError(w http.ResponseWriter, code int, msg string, err error) { + w.Header().Set("X-Vise", msg + ": " + err.Error()) + w.Header().Set("Content-Length", "0") + w.WriteHeader(code) + _, err = w.Write([]byte{}) + if err != nil { + w.WriteHeader(500) + w.Header().Set("X-Vise", err.Error()) + } + return +} + +func(f* SessionHandler) Shutdown() { + err := f.provider.Close() + if err != nil { + logg.Errorf("handler shutdown error", "err", err) + } +} + +func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + var r bool + sessionId, err := f.rp.GetSessionId(req) + if err != nil { + f.writeError(w, 400, "Session missing", err) + return + } + input, err := f.rp.GetInput(req) + if err != nil { + f.writeError(w, 400, "Input read fail", err) + return + } + ctx := req.Context() + cfg := f.cfgTemplate + cfg.SessionId = sessionId + + logg.InfoCtxf(ctx, "new request", "session", cfg.SessionId, "input", input) + + storage, err := f.provider.Get(cfg.SessionId) + if err != nil { + f.writeError(w, 500, "Storage retrieval fail", err) + return + } + f.hn = f.hn.WithPersister(storage.Persister) + defer f.provider.Put(cfg.SessionId, storage) + en := getEngine(cfg, f.rs, storage.Persister) + en = en.WithFirst(f.hn.Init) + if cfg.EngineDebug { + en = en.WithDebug(nil) + } + + r, err = en.Init(ctx) + if err != nil { + f.writeError(w, 500, "Engine init fail", err) + return + } + if r && len(input) > 0 { + r, err = en.Exec(ctx, input) + } + if err != nil { + f.writeError(w, 500, "Engine exec fail", err) + return + } + + w.WriteHeader(200) + w.Header().Set("Content-Type", "text/plain") + _, err = en.WriteResult(ctx, w) + if err != nil { + f.writeError(w, 500, "Write result fail", err) + return + } + err = en.Finish() + if err != nil { + f.writeError(w, 500, "Engine finish fail", err) + return + } + + _ = r +} + +func getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine { + en := engine.NewEngine(cfg, rs) + en = en.WithPersister(pr) + return en +} diff --git a/internal/http/storage.go b/internal/http/storage.go new file mode 100644 index 0000000..9b0cf44 --- /dev/null +++ b/internal/http/storage.go @@ -0,0 +1,44 @@ +package http + +import ( + "git.defalsify.org/vise.git/db" + "git.defalsify.org/vise.git/persist" +) + +type Storage struct { + Persister *persist.Persister + UserdataDb db.Db +} + +type StorageProvider interface { + Get(sessionId string) (Storage, error) + Put(sessionId string, storage Storage) error + Close() error +} + +type SimpleStorageProvider struct { + Storage +} + +func NewSimpleStorageProvider(stateStore db.Db, userdataStore db.Db) StorageProvider { + pe := persist.NewPersister(stateStore) + pe = pe.WithFlush() + return &SimpleStorageProvider{ + Storage: Storage{ + Persister: pe, + UserdataDb: userdataStore, + }, + } +} + +func (p *SimpleStorageProvider) Get(sessionId string) (Storage, error) { + return p.Storage, nil +} + +func (p *SimpleStorageProvider) Put(sessionId string, storage Storage) error { + return nil +} + +func (p *SimpleStorageProvider) Close() error { + return p.Storage.UserdataDb.Close() +}