diff --git a/cmd/http/main.go b/cmd/http/main.go new file mode 100644 index 0000000..d253c12 --- /dev/null +++ b/cmd/http/main.go @@ -0,0 +1,199 @@ +package main + +import ( + "context" + "flag" + "fmt" + "net/http" + "os" + "path" + "strconv" + + "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/persist" + "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 getStateStore(dbDir string, ctx context.Context) (db.Db, error) { + err := os.MkdirAll(dbDir, 0700) + if err != nil { + return nil, fmt.Errorf("state dir create exited with error: %v\n", err) + } + 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 getEngine(cfg engine.Config, rs resource.Resource, pr *persist.Persister) *engine.DefaultEngine { + en := engine.NewEngine(cfg, rs) + en = en.WithPersister(pr) + return en +} + +func main() { + var dbDir string + var resourceDir string + var size uint + var sessionId string + var engineDebug bool + var stateDebug bool + var host string + var port uint + flag.StringVar(&sessionId, "session-id", "075xx2123", "session id") + 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() + ctx = context.WithValue(ctx, "SessionId",sessionId) + pfp := path.Join(scriptDir, "pp.csv") + flagParser, err := getFlags(pfp, true) + + if err != nil { + os.Exit(1) + } + + cfg := engine.Config{ + Root: "root", + SessionId: sessionId, + 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) + } + + userdataStore := getUserdataDb(dbDir, ctx) + if err != nil { + fmt.Fprintf(os.Stderr, err.Error()) + os.Exit(1) + } + + 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) + } + + sh := httpserver.NewSessionHandler(cfg, rs, userdataStore, stateStore, hl.Init) + s := &http.Server{ + Addr: fmt.Sprintf("%s:%s", host, strconv.Itoa(int(port))), + Handler: sh, + } + + err = s.ListenAndServe() + if err != nil { + fmt.Fprintf(os.Stderr, "Server error: %s", err) + os.Exit(1) + } +} diff --git a/cmd/main.go b/cmd/main.go index fc734d4..9547dc4 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) @@ -145,7 +146,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 +160,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 e2aff05..dc20ac5 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.2.0.20240907200911-15fe28c9d5b0 + 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,9 +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 - gopkg.in/leonelquinteros/gotext.v1 v1.3.1 -) diff --git a/go.sum b/go.sum index 2624d07..d065871 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.2.0.20240907200911-15fe28c9d5b0 h1:B9kE2XXjrYmHNIgRV6fR1WLWE8+z8OvDhJSc96lbGPQ= +git.defalsify.org/vise.git v0.1.0-rc.2.0.20240907200911-15fe28c9d5b0/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 3b441c2..3eef63d 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..aa53448 --- /dev/null +++ b/internal/http/server.go @@ -0,0 +1,130 @@ +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" +) + +var ( + logg = logging.NewVanilla().WithDomain("httpserver") +) + +type RequestParser struct { +} + +func(rp *RequestParser) 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 *RequestParser) 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 + provider StorageProvider +} + +func NewSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, first resource.EntryFunc) *SessionHandler { + return &SessionHandler{ + cfgTemplate: cfg, + rs: rs, + first: first, + rp: RequestParser{}, + 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) 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 + } + en := getEngine(cfg, f.rs, storage.Persister) + en = en.WithFirst(f.first) + 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..012c56c --- /dev/null +++ b/internal/http/storage.go @@ -0,0 +1,43 @@ +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) + 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 nil +}