Compare commits
111 Commits
Author | SHA1 | Date | |
---|---|---|---|
1f0568df32 | |||
|
24c513d4f0 | ||
b3fd6f5c1a | |||
5734011f96 | |||
|
379d98ccd5 | ||
f40e11c267 | |||
b698f08136 | |||
4d7589ad95 | |||
efdb52bccd | |||
2ff9fed3c5 | |||
477b4cf8f6 | |||
ed6651697a | |||
c359d99075 | |||
8d477356f3 | |||
7f3294a8a2 | |||
|
721f80d0f2 | ||
f49e54a562 | |||
|
5081b6d4ce | ||
4d72ae0313 | |||
4fe64a7747 | |||
3004698d5b | |||
50c7ff1046 | |||
07b061a68b | |||
6339f0c2e5 | |||
|
1fa830f286 | ||
64fba91670 | |||
c15958a1ad | |||
ee442daefa | |||
656052dc74 | |||
6c5873da6f | |||
11d30583a4 | |||
f83f539046 | |||
562bd4fa24 | |||
90df0eefc3 | |||
b37f2a0a11 | |||
68e4c9af03 | |||
|
599815c343 | ||
|
462c0d7677 | ||
80b96e9bf6 | |||
b5561decd1 | |||
|
02823fd64e | ||
|
cd575c2edb | ||
f3d4f35718 | |||
52787bdb4d | |||
824d39908b | |||
|
52fd1eced2 | ||
a312ea5b84 | |||
|
5c7a535288 | ||
4836162f40 | |||
|
cc2f7b41df | ||
|
d39740a09a | ||
|
2024cc96e2 | ||
|
d2d878d5d7 | ||
c995143543 | |||
44570e20ef | |||
362eb209ef | |||
c69d3896f1 | |||
974af6b2a7 | |||
|
bb4037e73f | ||
|
51b6fc0dde | ||
|
cc9760125a | ||
|
3a9f3fa373 | ||
|
89c21847b9 | ||
|
450dfa02cc | ||
|
f61e65f4fe | ||
|
a4d6cef9c0 | ||
|
2992f7ae8e | ||
|
dc61d05584 | ||
|
83857026d3 | ||
|
349051b5ef | ||
47b5ff0435 | |||
|
e92e498726 | ||
|
25867cf05e | ||
|
c3cbe1cd92 | ||
|
418080d093 | ||
|
2e30739ec9 | ||
d5a2680500 | |||
|
dc1674ec55 | ||
|
d950b10b50 | ||
|
bcb3ab905e | ||
|
3ed9caf16d | ||
|
86464c31d2 | ||
5ee10d8e14 | |||
62f3681b9e | |||
3ce1435591 | |||
f65c458daa | |||
|
67007fcd48 | ||
|
f1b258fa6d | ||
|
daec816a3e | ||
|
ac0c43cb43 | ||
|
9013cc3618 | ||
|
056d056613 | ||
|
e581ec4771 | ||
|
e16b7445e8 | ||
|
1b12f0ba5f | ||
d2fce05461 | |||
68ac237449 | |||
162e6c1934 | |||
8bd025f2b2 | |||
|
c1e0617bb3 | ||
|
6723884103 | ||
|
b888af446d | ||
|
43b2c3b78d | ||
|
d67853f6d9 | ||
|
06230dc557 | ||
|
6ee2c88fe2 | ||
|
bb1a846cb3 | ||
|
967e53d83b | ||
|
d246cdee51 | ||
|
d518a76536 | ||
|
6f65c33be4 |
@ -1,5 +1,6 @@
|
||||
/**
|
||||
!/cmd/africastalking
|
||||
!/cmd/ssh
|
||||
!/common
|
||||
!/config
|
||||
!/initializers
|
||||
|
14
.env.example
14
.env.example
@ -6,15 +6,15 @@ HOST=127.0.0.1
|
||||
AT_ENDPOINT=/ussd/africastalking
|
||||
|
||||
#PostgreSQL
|
||||
DB_HOST=localhost
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=strongpass
|
||||
DB_NAME=urdt_ussd
|
||||
DB_PORT=5432
|
||||
DB_SSLMODE=disable
|
||||
DB_TIMEZONE=Africa/Nairobi
|
||||
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
|
||||
#DB_TIMEZONE=Africa/Nairobi
|
||||
#DB_SCHEMA=vise
|
||||
|
||||
#External API Calls
|
||||
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||
BEARER_TOKEN=eyJeSIsInRcCI6IkpXVCJ.yJwdWJsaWNLZXkiOiIwrrrrrr
|
||||
DATA_URL_BASE=http://localhost:5006
|
||||
|
||||
#Language
|
||||
DEFAULT_LANGUAGE=eng
|
||||
LANGUAGES=eng, swa
|
||||
|
@ -19,6 +19,7 @@ WORKDIR /build
|
||||
RUN echo "Building on $BUILDPLATFORM, building for $TARGETPLATFORM"
|
||||
RUN go mod download
|
||||
RUN go build -tags logtrace -o ussd-africastalking -ldflags="-X main.build=${BUILD} -s -w" cmd/africastalking/main.go
|
||||
RUN go build -tags logtrace -o ussd-ssh -ldflags="-X main.build=${BUILD} -s -w" cmd/ssh/main.go
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
@ -30,6 +31,7 @@ RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
WORKDIR /service
|
||||
|
||||
COPY --from=build /build/ussd-africastalking .
|
||||
COPY --from=build /build/ussd-ssh .
|
||||
COPY --from=build /build/LICENSE .
|
||||
COPY --from=build /build/README.md .
|
||||
COPY --from=build /build/services ./services
|
||||
@ -37,5 +39,6 @@ COPY --from=build /build/.env.example .
|
||||
RUN mv .env.example .env
|
||||
|
||||
EXPOSE 7123
|
||||
EXPOSE 7122
|
||||
|
||||
CMD ["./ussd-africastalking"]
|
@ -1,35 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/http/at"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
|
||||
scriptDir = path.Join("services", "registration")
|
||||
build = "dev"
|
||||
menuSeparator = ": "
|
||||
@ -39,94 +36,50 @@ func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
type atRequestParser struct{}
|
||||
|
||||
func (arp *atRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
logg.Warnf("got an invalid request", "req", rq)
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Capture body (if any) for logging
|
||||
body, err := io.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to read request body", "err", err)
|
||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
// Reset the body for further reading
|
||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Log the body as JSON
|
||||
bodyLog := map[string]string{"body": string(body)}
|
||||
logBytes, err := json.Marshal(bodyLog)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to marshal request body", "err", err)
|
||||
} else {
|
||||
logg.Debugf("received request", "bytes", logBytes)
|
||||
}
|
||||
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
logg.Warnf("failed to parse form data", "err", err)
|
||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
phoneNumber := rqv.FormValue("phoneNumber")
|
||||
if phoneNumber == "" {
|
||||
return "", fmt.Errorf("no phone number found")
|
||||
}
|
||||
|
||||
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to format phone number", "err", err)
|
||||
return "", fmt.Errorf("failed to format number")
|
||||
}
|
||||
|
||||
return formattedNumber, nil
|
||||
}
|
||||
|
||||
func (arp *atRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
text := rqv.FormValue("text")
|
||||
|
||||
parts := strings.Split(text, "*")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("no input found")
|
||||
}
|
||||
|
||||
return []byte(parts[len(parts)-1]), nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var connStr string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
var err error
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
@ -140,14 +93,13 @@ func main() {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
@ -191,9 +143,9 @@ func main() {
|
||||
}
|
||||
defer stateStore.Close()
|
||||
|
||||
rp := &atRequestParser{}
|
||||
rp := &at.ATRequestParser{}
|
||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||
sh := httpserver.NewATSessionHandler(bsh)
|
||||
sh := at.NewATSessionHandler(bsh)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
|
||||
|
@ -12,12 +12,14 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -35,7 +37,7 @@ type asyncRequestParser struct {
|
||||
input []byte
|
||||
}
|
||||
|
||||
func (p *asyncRequestParser) GetSessionId(r any) (string, error) {
|
||||
func (p *asyncRequestParser) GetSessionId(ctx context.Context, r any) (string, error) {
|
||||
return p.sessionId, nil
|
||||
}
|
||||
|
||||
@ -46,28 +48,50 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var connStr string
|
||||
var sessionId string
|
||||
var dbDir string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
var err error
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
|
||||
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.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
@ -81,18 +105,18 @@ func main() {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
@ -21,6 +22,7 @@ import (
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -36,26 +38,48 @@ func init() {
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var connStr string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
var err error
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData, "resourcedir", resourceDir, "outputsize", size)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
@ -69,14 +93,9 @@ func main() {
|
||||
cfg.EngineDebug = true
|
||||
}
|
||||
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
err = menuStorageService.EnsureDbDir()
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
|
48
cmd/main.go
48
cmd/main.go
@ -10,10 +10,12 @@ import (
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
"git.grassecon.net/urdt/ussd/internal/args"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
@ -27,26 +29,55 @@ func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
// TODO: external script automatically generate language handler list from select language vise code OR consider dynamic menu generation script possibility
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var connStr string
|
||||
var size uint
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var resourceDir string
|
||||
var err error
|
||||
var gettextDir string
|
||||
var langs args.LangVar
|
||||
|
||||
flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&gettextDir, "gettext", "", "use gettext translations from given directory")
|
||||
flag.Var(&langs, "language", "add symbol resolution for language")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData, "outputsize", size)
|
||||
|
||||
if len(langs.Langs()) == 0 {
|
||||
langs.Set(config.DefaultLanguage)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
ln, err := lang.LanguageFromCode(config.DefaultLanguage)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "default language set error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
ctx = context.WithValue(ctx, "Language", ln)
|
||||
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
cfg := engine.Config{
|
||||
@ -57,13 +88,10 @@ func main() {
|
||||
MenuSeparator: menuSeparator,
|
||||
}
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
if gettextDir != "" {
|
||||
menuStorageService = menuStorageService.WithGettext(gettextDir, langs.Langs())
|
||||
}
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
|
34
cmd/ssh/README.md
Normal file
34
cmd/ssh/README.md
Normal file
@ -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 <session_id> [--dbdir <dbpath>] <client_publickey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Create a private key for the server
|
||||
|
||||
```
|
||||
ssh-keygen -N "" -f <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Run the server
|
||||
|
||||
|
||||
```
|
||||
go run -v -tags logtrace ./cmd/ssh/main.go -h <host> -p <port> [--dbdir <dbpath>] <server_privatekey_filepath>
|
||||
```
|
||||
|
||||
|
||||
## Connect to the server
|
||||
|
||||
```
|
||||
ssh [-v] -T -p <port> -i <client_publickey_filepath> <host>
|
||||
```
|
144
cmd/ssh/main.go
Normal file
144
cmd/ssh/main.go
Normal file
@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"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/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
"git.grassecon.net/urdt/ussd/internal/ssh"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
keyStore db.Db
|
||||
logg = logging.NewVanilla()
|
||||
scriptDir = path.Join("services", "registration")
|
||||
|
||||
build = "dev"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var connStr string
|
||||
var authConnStr string
|
||||
var resourceDir string
|
||||
var size uint
|
||||
var engineDebug bool
|
||||
var stateDebug bool
|
||||
var host string
|
||||
var port uint
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.StringVar(&authConnStr, "authdb", "", "auth connection string")
|
||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.UintVar(&size, "s", 160, "max size of output")
|
||||
flag.StringVar(&host, "h", "127.0.0.1", "socket host")
|
||||
flag.UintVar(&port, "p", 7122, "socket port")
|
||||
flag.Parse()
|
||||
|
||||
if connStr == "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
if authConnStr == "" {
|
||||
authConnStr = connStr
|
||||
}
|
||||
connData, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
authConnData, err := storage.ToConnData(authConnStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "auth connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
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", "conn", connData, "authconn", authConnData, "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, authConnData.String())
|
||||
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,
|
||||
Conn: connData,
|
||||
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)
|
||||
}
|
44
cmd/ssh/sshkey/main.go
Normal file
44
cmd/ssh/sshkey/main.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
@ -55,6 +55,8 @@ const (
|
||||
DATA_ACTIVE_DECIMAL
|
||||
// EVM address of the currently active voucher
|
||||
DATA_ACTIVE_ADDRESS
|
||||
//Holds count of the number of incorrect PIN attempts
|
||||
DATA_INCORRECT_PIN_ATTEMPTS
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -6,9 +6,13 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Define the regex pattern as a constant
|
||||
const (
|
||||
// Define the regex pattern as a constant
|
||||
pinPattern = `^\d{4}$`
|
||||
|
||||
//Allowed incorrect PIN attempts
|
||||
AllowedPINAttempts = uint8(3)
|
||||
|
||||
)
|
||||
|
||||
// checks whether the given input is a 4 digit number
|
||||
|
@ -8,31 +8,32 @@ import (
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
)
|
||||
|
||||
func StoreToDb(store *UserDataStore) db.Db {
|
||||
return store.Db
|
||||
}
|
||||
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
|
||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||
}
|
||||
|
||||
type StorageServices interface {
|
||||
GetPersister(ctx context.Context) (*persist.Persister, error)
|
||||
GetUserdataDb(ctx context.Context) (db.Db, error)
|
||||
GetResource(ctx context.Context) (resource.Resource, error)
|
||||
EnsureDbDir() error
|
||||
}
|
||||
|
||||
type StorageService struct {
|
||||
svc *storage.MenuStorageService
|
||||
}
|
||||
|
||||
func NewStorageService(dbDir string) *StorageService {
|
||||
return &StorageService{
|
||||
svc: storage.NewMenuStorageService(dbDir, ""),
|
||||
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
|
||||
svc := &StorageService{
|
||||
svc: storage.NewMenuStorageService(conn, ""),
|
||||
}
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
||||
@ -46,7 +47,3 @@ func(ss *StorageService) GetUserdataDb(ctx context.Context) (db.Db, error) {
|
||||
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func(ss *StorageService) EnsureDbDir() error {
|
||||
return ss.svc.EnsureDbDir()
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -56,7 +56,7 @@ func ProcessTransfers(transfers []dataserviceapi.Last10TxResponse) TransferMetad
|
||||
|
||||
// GetTransferData retrieves and matches transfer data
|
||||
// returns a formatted string of the full transaction/statement
|
||||
func GetTransferData(ctx context.Context, db storage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
func GetTransferData(ctx context.Context, db dbstorage.PrefixDb, publicKey string, index int) (string, error) {
|
||||
keys := []DataTyp{DATA_TX_SENDERS, DATA_TX_RECIPIENTS, DATA_TX_VALUES, DATA_TX_ADDRESSES, DATA_TX_HASHES, DATA_TX_DATES, DATA_TX_SYMBOLS}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -63,7 +63,7 @@ func ScaleDownBalance(balance, decimals string) string {
|
||||
}
|
||||
|
||||
// GetVoucherData retrieves and matches voucher data
|
||||
func GetVoucherData(ctx context.Context, db storage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
func GetVoucherData(ctx context.Context, db dbstorage.PrefixDb, input string) (*dataserviceapi.TokenHoldings, error) {
|
||||
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
||||
data := make(map[DataTyp]string)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
|
||||
visedb "git.defalsify.org/vise.git/db"
|
||||
memdb "git.defalsify.org/vise.git/db/mem"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
@ -86,7 +86,7 @@ func TestGetVoucherData(t *testing.T) {
|
||||
}
|
||||
|
||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
// Test voucher data
|
||||
mockData := map[DataTyp][]byte{
|
||||
|
@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
)
|
||||
@ -18,6 +19,11 @@ const (
|
||||
AliasPrefix = "api/v1/alias"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLanguage = "eng"
|
||||
languages []string
|
||||
)
|
||||
|
||||
var (
|
||||
custodialURLBase string
|
||||
dataURLBase string
|
||||
@ -34,8 +40,29 @@ var (
|
||||
VoucherTransfersURL string
|
||||
VoucherDataURL string
|
||||
CheckAliasURL string
|
||||
DbConn string
|
||||
DefaultLanguage string
|
||||
Languages []string
|
||||
)
|
||||
|
||||
func setLanguage() error {
|
||||
defaultLanguage = initializers.GetEnv("DEFAULT_LANGUAGE", defaultLanguage)
|
||||
languages = strings.Split(initializers.GetEnv("LANGUAGES", defaultLanguage), ",")
|
||||
haveDefaultLanguage := false
|
||||
for i, v := range(languages) {
|
||||
languages[i] = strings.ReplaceAll(v, " ", "")
|
||||
if languages[i] == defaultLanguage {
|
||||
haveDefaultLanguage = true
|
||||
}
|
||||
}
|
||||
|
||||
if !haveDefaultLanguage {
|
||||
languages = append([]string{defaultLanguage}, languages...)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setBase() error {
|
||||
var err error
|
||||
|
||||
@ -43,14 +70,20 @@ func setBase() error {
|
||||
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
|
||||
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
|
||||
|
||||
_, err = url.JoinPath(custodialURLBase, "/foo")
|
||||
_, err = url.Parse(custodialURLBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = url.JoinPath(dataURLBase, "/bar")
|
||||
_, err = url.Parse(dataURLBase)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func setConn() error {
|
||||
DbConn = initializers.GetEnv("DB_CONN", "")
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -60,6 +93,14 @@ func LoadConfig() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = setLanguage()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
||||
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
||||
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
||||
@ -69,6 +110,8 @@ func LoadConfig() error {
|
||||
VoucherTransfersURL, _ = url.JoinPath(dataURLBase, voucherTransfersPathPrefix)
|
||||
VoucherDataURL, _ = url.JoinPath(dataURLBase, voucherDataPathPrefix)
|
||||
CheckAliasURL, _ = url.JoinPath(dataURLBase, AliasPrefix)
|
||||
DefaultLanguage = defaultLanguage
|
||||
Languages = languages
|
||||
|
||||
return nil
|
||||
}
|
||||
|
126
devtools/lang/main.go
Normal file
126
devtools/lang/main.go
Normal file
@ -0,0 +1,126 @@
|
||||
// create language files from environment
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.grassecon.net/urdt/ussd/config"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
changeHeadSrc = `LOAD reset_account_authorized 0
|
||||
LOAD reset_incorrect 0
|
||||
CATCH incorrect_pin flag_incorrect_pin 1
|
||||
CATCH pin_entry flag_account_authorized 0
|
||||
`
|
||||
|
||||
selectSrc = `LOAD set_language 6
|
||||
RELOAD set_language
|
||||
CATCH terms flag_account_created 0
|
||||
MOVE language_changed
|
||||
`
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla()
|
||||
mouts string
|
||||
incmps string
|
||||
)
|
||||
|
||||
func init() {
|
||||
initializers.LoadEnvVariables()
|
||||
}
|
||||
|
||||
func toLanguageLabel(ln lang.Language) string {
|
||||
s := ln.Name
|
||||
v := strings.Split(s, " (")
|
||||
if len(v) > 1 {
|
||||
s = v[0]
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func toLanguageKey(ln lang.Language) string {
|
||||
s := toLanguageLabel(ln)
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var srcDir string
|
||||
|
||||
flag.StringVar(&srcDir, "o", ".", "resource dir write to")
|
||||
flag.Parse()
|
||||
|
||||
logg.Infof("start command", "dir", srcDir)
|
||||
|
||||
err := config.LoadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "config load error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
logg.Tracef("using languages", "lang", config.Languages)
|
||||
|
||||
for i, v := range(config.Languages) {
|
||||
ln, err := lang.LanguageFromCode(v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error parsing language: %s\n", v)
|
||||
os.Exit(1)
|
||||
}
|
||||
n := i + 1
|
||||
s := toLanguageKey(ln)
|
||||
mouts += fmt.Sprintf("MOUT %s %v\n", s, n)
|
||||
v = "set_" + ln.Code
|
||||
incmps += fmt.Sprintf("INCMP %s %v\n", v, n)
|
||||
|
||||
p := path.Join(srcDir, v)
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open language set template output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
s = toLanguageLabel(ln)
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(s))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
src := mouts + "HALT\n" + incmps
|
||||
src += "INCMP . *\n"
|
||||
|
||||
p := path.Join(srcDir, "select_language.vis")
|
||||
w, err := os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
src = changeHeadSrc + src
|
||||
p = path.Join(srcDir, "change_language.vis")
|
||||
w, err = os.OpenFile(p, os.O_WRONLY | os.O_CREATE | os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed open select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer w.Close()
|
||||
_, err = w.Write([]byte(src))
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed write select language vis output: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
@ -25,26 +25,46 @@ func init() {
|
||||
}
|
||||
|
||||
|
||||
func formatItem(k []byte, v []byte) (string, error) {
|
||||
o, err := debug.FromKey(k)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s := fmt.Sprintf("%vValue: %v\n\n", o, string(v))
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var connStr string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var err error
|
||||
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&connStr, "c", ".state", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.Parse()
|
||||
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(config.DbConn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
@ -64,12 +84,12 @@ func main() {
|
||||
if k == nil {
|
||||
break
|
||||
}
|
||||
o, err := debug.FromKey(k)
|
||||
r, err := formatItem(k, v)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "format db item error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("%vValue: %v\n\n", o, string(v))
|
||||
fmt.Printf(r)
|
||||
}
|
||||
|
||||
err = store.Close()
|
@ -28,23 +28,34 @@ func init() {
|
||||
func main() {
|
||||
config.LoadConfig()
|
||||
|
||||
var dbDir string
|
||||
var connStr string
|
||||
var sessionId string
|
||||
var database string
|
||||
var engineDebug bool
|
||||
var err error
|
||||
|
||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
||||
flag.StringVar(&connStr, "c", "", "connection string")
|
||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||
flag.Parse()
|
||||
|
||||
if connStr != "" {
|
||||
connStr = config.DbConn
|
||||
}
|
||||
connData, err := storage.ToConnData(config.DbConn)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logg.Infof("start command", "conn", connData)
|
||||
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", database)
|
||||
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||
|
||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
4
go.mod
4
go.mod
@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d
|
||||
github.com/alecthomas/assert/v2 v2.2.2
|
||||
github.com/gofrs/uuid v4.4.0+incompatible
|
||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||
@ -11,6 +11,7 @@ require (
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/peteole/testdata-loader v0.3.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.27.0
|
||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||
)
|
||||
|
||||
@ -32,7 +33,6 @@ require (
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
golang.org/x/crypto v0.27.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/text v0.18.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
4
go.sum
4
go.sum
@ -1,5 +1,5 @@
|
||||
git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9 h1:O3m+NgWDWtJm8OculT99c4bDMAO4xLe2c8hpCKpsd9g=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20241231085136-8582c7e157d9/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||
git.defalsify.org/vise.git v0.2.3-0.20250103172917-3e190a44568d/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||
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=
|
||||
|
34
internal/args/lang.go
Normal file
34
internal/args/lang.go
Normal file
@ -0,0 +1,34 @@
|
||||
package args
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
)
|
||||
|
||||
type LangVar struct {
|
||||
v []lang.Language
|
||||
}
|
||||
|
||||
func(lv *LangVar) Set(s string) error {
|
||||
v, err := lang.LanguageFromCode(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lv.v = append(lv.v, v)
|
||||
return err
|
||||
}
|
||||
|
||||
func(lv *LangVar) String() string {
|
||||
var s []string
|
||||
for _, v := range(lv.v) {
|
||||
s = append(s, v.Code)
|
||||
}
|
||||
return strings.Join(s, ",")
|
||||
}
|
||||
|
||||
func(lv *LangVar) Langs() []lang.Language {
|
||||
return lv.v
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package ussd
|
||||
package application
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -23,12 +23,12 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
"gopkg.in/leonelquinteros/gotext.v1"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler").WithContextKey("SessionId")
|
||||
scriptDir = path.Join("services", "registration")
|
||||
translationDir = path.Join(scriptDir, "locale")
|
||||
)
|
||||
@ -64,7 +64,7 @@ type Handlers struct {
|
||||
adminstore *utils.AdminStore
|
||||
flagManager *asm.FlagParser
|
||||
accountService remote.AccountServiceInterface
|
||||
prefixDb storage.PrefixDb
|
||||
prefixDb dbstorage.PrefixDb
|
||||
profile *models.Profile
|
||||
ReplaceSeparatorFunc func(string) string
|
||||
}
|
||||
@ -80,7 +80,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
||||
|
||||
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
||||
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
||||
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
|
||||
prefixDb := dbstorage.NewSubPrefixDb(userdataStore, prefix)
|
||||
|
||||
h := &Handlers{
|
||||
userdataStore: userDb,
|
||||
@ -122,9 +122,12 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
|
||||
h.st.Code = []byte{}
|
||||
}
|
||||
|
||||
sessionId, _ := ctx.Value("SessionId").(string)
|
||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
if ok {
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
}
|
||||
|
||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
||||
isAdmin, _ := h.adminstore.IsAdmin(sessionId)
|
||||
|
||||
if isAdmin {
|
||||
@ -731,11 +734,23 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
||||
if h.st.MatchFlag(flag_account_authorized, false) {
|
||||
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
||||
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
|
||||
err := h.resetIncorrectPINAttempts(ctx, sessionId)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
} else {
|
||||
res.FlagSet = append(res.FlagSet, flag_allow_update)
|
||||
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
||||
err := h.resetIncorrectPINAttempts(ctx, sessionId)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
err := h.incrementIncorrectPINAttempts(ctx, sessionId)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
||||
res.FlagReset = append(res.FlagReset, flag_account_authorized)
|
||||
return res, nil
|
||||
@ -749,8 +764,34 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
||||
// ResetIncorrectPin resets the incorrect pin flag after a new PIN attempt.
|
||||
func (h *Handlers) ResetIncorrectPin(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
store := h.userdataStore
|
||||
|
||||
flag_incorrect_pin, _ := h.flagManager.GetFlag("flag_incorrect_pin")
|
||||
flag_account_blocked, _ := h.flagManager.GetFlag("flag_account_blocked")
|
||||
|
||||
sessionId, ok := ctx.Value("SessionId").(string)
|
||||
if !ok {
|
||||
return res, fmt.Errorf("missing session")
|
||||
}
|
||||
|
||||
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
||||
|
||||
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
|
||||
if err != nil {
|
||||
if !db.IsNotFound(err) {
|
||||
return res, err
|
||||
}
|
||||
}
|
||||
pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
|
||||
remainingPINAttempts := common.AllowedPINAttempts - uint8(pinAttemptsValue)
|
||||
if remainingPINAttempts == 0 {
|
||||
res.FlagSet = append(res.FlagSet, flag_account_blocked)
|
||||
return res, nil
|
||||
}
|
||||
if remainingPINAttempts < common.AllowedPINAttempts {
|
||||
res.Content = strconv.Itoa(int(remainingPINAttempts))
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
@ -837,6 +878,16 @@ func (h *Handlers) QuitWithHelp(ctx context.Context, sym string, input []byte) (
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// ShowBlockedAccount displays a message after an account has been blocked and how to reach support.
|
||||
func (h *Handlers) ShowBlockedAccount(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
code := codeFromCtx(ctx)
|
||||
l := gotext.NewLocale(translationDir, code)
|
||||
l.AddDomain("default")
|
||||
res.Content = l.Get("Your account has been locked. For help on how to unblock your account, contact support at: 0757628885")
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// VerifyYob verifies the length of the given input.
|
||||
func (h *Handlers) VerifyYob(ctx context.Context, sym string, input []byte) (resource.Result, error) {
|
||||
var res resource.Result
|
||||
@ -2072,3 +2123,53 @@ func (h *Handlers) UpdateAllProfileItems(ctx context.Context, sym string, input
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// incrementIncorrectPINAttempts keeps track of the number of incorrect PIN attempts
|
||||
func (h *Handlers) incrementIncorrectPINAttempts(ctx context.Context, sessionId string) error {
|
||||
var pinAttemptsCount uint8
|
||||
store := h.userdataStore
|
||||
|
||||
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
//First time Wrong PIN attempt: initialize with a count of 1
|
||||
pinAttemptsCount = 1
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount))))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", currentWrongPinAttempts, "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
pinAttemptsValue, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
|
||||
pinAttemptsCount = uint8(pinAttemptsValue) + 1
|
||||
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(pinAttemptsCount))))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to write incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", pinAttemptsCount, "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// resetIncorrectPINAttempts resets the number of incorrect PIN attempts after a correct PIN entry
|
||||
func (h *Handlers) resetIncorrectPINAttempts(ctx context.Context, sessionId string) error {
|
||||
store := h.userdataStore
|
||||
currentWrongPinAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
|
||||
if err != nil {
|
||||
if db.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
currentWrongPinAttemptsCount, _ := strconv.ParseUint(string(currentWrongPinAttempts), 0, 64)
|
||||
if currentWrongPinAttemptsCount <= uint64(common.AllowedPINAttempts) {
|
||||
err = store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("0")))
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(ctx, "failed to reset incorrect PIN attempts ", "key", common.DATA_INCORRECT_PIN_ATTEMPTS, "value", common.AllowedPINAttempts, "error", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
package ussd
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@ -13,7 +14,7 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/state"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
@ -59,14 +60,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
|
||||
return ctx, store
|
||||
}
|
||||
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *storage.SubPrefixDb {
|
||||
func InitializeTestSubPrefixDb(t *testing.T, ctx context.Context) *dbstorage.SubPrefixDb {
|
||||
db := memdb.NewMemDb()
|
||||
err := db.Connect(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
||||
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||
|
||||
return spdb
|
||||
}
|
||||
@ -907,37 +908,79 @@ func TestResetAccountAuthorized(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIncorrectPinReset(t *testing.T) {
|
||||
sessionId := "session123"
|
||||
ctx, store := InitializeTestStore(t)
|
||||
fm, err := NewFlagManager(flagsPath)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
flag_incorrect_pin, _ := fm.parser.GetFlag("flag_incorrect_pin")
|
||||
flag_account_blocked, _ := fm.parser.GetFlag("flag_account_blocked")
|
||||
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
|
||||
// Define test cases
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
attempts uint8
|
||||
expectedResult resource.Result
|
||||
}{
|
||||
{
|
||||
name: "Test incorrect pin reset",
|
||||
name: "Test when incorrect PIN attempts is 2",
|
||||
input: []byte(""),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_incorrect_pin},
|
||||
Content: "1", //Expected remaining PIN attempts
|
||||
},
|
||||
attempts: 2,
|
||||
},
|
||||
{
|
||||
name: "Test incorrect pin reset when incorrect PIN attempts is 1",
|
||||
input: []byte(""),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_incorrect_pin},
|
||||
Content: "2", //Expected remaining PIN attempts
|
||||
},
|
||||
attempts: 1,
|
||||
},
|
||||
{
|
||||
name: "Test incorrect pin reset when incorrect PIN attempts is 1",
|
||||
input: []byte(""),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_incorrect_pin},
|
||||
Content: "2", //Expected remaining PIN attempts
|
||||
},
|
||||
attempts: 1,
|
||||
},
|
||||
{
|
||||
name: "Test incorrect pin reset when incorrect PIN attempts is 3(account expected to be blocked)",
|
||||
input: []byte(""),
|
||||
expectedResult: resource.Result{
|
||||
FlagReset: []uint32{flag_incorrect_pin},
|
||||
FlagSet: []uint32{flag_account_blocked},
|
||||
},
|
||||
attempts: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
if err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(tt.attempts)))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create the Handlers instance with the mock flag manager
|
||||
h := &Handlers{
|
||||
flagManager: fm.parser,
|
||||
userdataStore: store,
|
||||
}
|
||||
|
||||
// Call the method
|
||||
res, err := h.ResetIncorrectPin(context.Background(), "reset_incorrect_pin", tt.input)
|
||||
res, err := h.ResetIncorrectPin(ctx, "reset_incorrect_pin", tt.input)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@ -2190,3 +2233,55 @@ func TestGetVoucherDetails(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedResult, res)
|
||||
}
|
||||
|
||||
func TestCountIncorrectPINAttempts(t *testing.T) {
|
||||
ctx, store := InitializeTestStore(t)
|
||||
sessionId := "session123"
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
attempts := uint8(2)
|
||||
|
||||
h := &Handlers{
|
||||
userdataStore: store,
|
||||
}
|
||||
err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(strconv.Itoa(int(attempts))))
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
}
|
||||
err = h.incrementIncorrectPINAttempts(ctx, sessionId)
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
}
|
||||
|
||||
attemptsAfterCount, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
}
|
||||
pinAttemptsValue, _ := strconv.ParseUint(string(attemptsAfterCount), 0, 64)
|
||||
pinAttemptsCount := uint8(pinAttemptsValue)
|
||||
expectedAttempts := attempts + 1
|
||||
assert.Equal(t, pinAttemptsCount, expectedAttempts)
|
||||
|
||||
}
|
||||
|
||||
func TestResetIncorrectPINAttempts(t *testing.T) {
|
||||
ctx, store := InitializeTestStore(t)
|
||||
sessionId := "session123"
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
|
||||
err := store.WriteEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS, []byte(string("2")))
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
}
|
||||
|
||||
h := &Handlers{
|
||||
userdataStore: store,
|
||||
}
|
||||
h.resetIncorrectPINAttempts(ctx, sessionId)
|
||||
incorrectAttempts, err := store.ReadEntry(ctx, sessionId, common.DATA_INCORRECT_PIN_ATTEMPTS)
|
||||
|
||||
if err != nil {
|
||||
t.Logf(err.Error())
|
||||
}
|
||||
assert.Equal(t, "0", string(incorrectAttempts))
|
||||
|
||||
}
|
@ -6,7 +6,7 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/application"
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
|
||||
@ -14,11 +14,11 @@ type BaseSessionHandler struct {
|
||||
cfgTemplate engine.Config
|
||||
rp RequestParser
|
||||
rs resource.Resource
|
||||
hn *ussd.Handlers
|
||||
hn *application.Handlers
|
||||
provider storage.StorageProvider
|
||||
}
|
||||
|
||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *ussd.Handlers) *BaseSessionHandler {
|
||||
func NewBaseSessionHandler(cfg engine.Config, rs resource.Resource, stateDb db.Db, userdataDb db.Db, rp RequestParser, hn *application.Handlers) *BaseSessionHandler {
|
||||
return &BaseSessionHandler{
|
||||
cfgTemplate: cfg,
|
||||
rs: rs,
|
||||
|
@ -10,13 +10,13 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/ussd"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers/application"
|
||||
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
type HandlerService interface {
|
||||
GetHandler() (*ussd.Handlers, error)
|
||||
GetHandler() (*application.Handlers, error)
|
||||
}
|
||||
|
||||
func getParser(fp string, debug bool) (*asm.FlagParser, error) {
|
||||
@ -64,72 +64,73 @@ func (ls *LocalHandlerService) SetDataStore(db *db.Db) {
|
||||
ls.UserdataStore = db
|
||||
}
|
||||
|
||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*ussd.Handlers, error) {
|
||||
func (ls *LocalHandlerService) GetHandler(accountService remote.AccountServiceInterface) (*application.Handlers, error) {
|
||||
replaceSeparatorFunc := func(input string) string {
|
||||
return strings.ReplaceAll(input, ":", ls.Cfg.MenuSeparator)
|
||||
}
|
||||
|
||||
ussdHandlers, err := ussd.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
|
||||
appHandlers, err := application.NewHandlers(ls.Parser, *ls.UserdataStore, ls.AdminStore, accountService, replaceSeparatorFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ussdHandlers = ussdHandlers.WithPersister(ls.Pe)
|
||||
ls.DbRs.AddLocalFunc("set_language", ussdHandlers.SetLanguage)
|
||||
ls.DbRs.AddLocalFunc("create_account", ussdHandlers.CreateAccount)
|
||||
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)
|
||||
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("invite_valid_recipient", ussdHandlers.InviteValidRecipient)
|
||||
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("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("verify_new_pin", ussdHandlers.VerifyNewPin)
|
||||
ls.DbRs.AddLocalFunc("confirm_pin_change", ussdHandlers.ConfirmPinChange)
|
||||
ls.DbRs.AddLocalFunc("quit_with_help", ussdHandlers.QuitWithHelp)
|
||||
ls.DbRs.AddLocalFunc("fetch_community_balance", ussdHandlers.FetchCommunityBalance)
|
||||
ls.DbRs.AddLocalFunc("set_default_voucher", ussdHandlers.SetDefaultVoucher)
|
||||
ls.DbRs.AddLocalFunc("check_vouchers", ussdHandlers.CheckVouchers)
|
||||
ls.DbRs.AddLocalFunc("get_vouchers", ussdHandlers.GetVoucherList)
|
||||
ls.DbRs.AddLocalFunc("view_voucher", ussdHandlers.ViewVoucher)
|
||||
ls.DbRs.AddLocalFunc("set_voucher", ussdHandlers.SetVoucher)
|
||||
ls.DbRs.AddLocalFunc("get_voucher_details", ussdHandlers.GetVoucherDetails)
|
||||
ls.DbRs.AddLocalFunc("reset_valid_pin", ussdHandlers.ResetValidPin)
|
||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", ussdHandlers.CheckBlockedNumPinMisMatch)
|
||||
ls.DbRs.AddLocalFunc("validate_blocked_number", ussdHandlers.ValidateBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("retrieve_blocked_number", ussdHandlers.RetrieveBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_unregistered_number", ussdHandlers.ResetUnregisteredNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_others_pin", ussdHandlers.ResetOthersPin)
|
||||
ls.DbRs.AddLocalFunc("save_others_temporary_pin", ussdHandlers.SaveOthersTemporaryPin)
|
||||
ls.DbRs.AddLocalFunc("get_current_profile_info", ussdHandlers.GetCurrentProfileInfo)
|
||||
ls.DbRs.AddLocalFunc("check_transactions", ussdHandlers.CheckTransactions)
|
||||
ls.DbRs.AddLocalFunc("get_transactions", ussdHandlers.GetTransactionsList)
|
||||
ls.DbRs.AddLocalFunc("view_statement", ussdHandlers.ViewTransactionStatement)
|
||||
ls.DbRs.AddLocalFunc("update_all_profile_items", ussdHandlers.UpdateAllProfileItems)
|
||||
ls.DbRs.AddLocalFunc("set_back", ussdHandlers.SetBack)
|
||||
appHandlers = appHandlers.WithPersister(ls.Pe)
|
||||
ls.DbRs.AddLocalFunc("set_language", appHandlers.SetLanguage)
|
||||
ls.DbRs.AddLocalFunc("create_account", appHandlers.CreateAccount)
|
||||
ls.DbRs.AddLocalFunc("save_temporary_pin", appHandlers.SaveTemporaryPin)
|
||||
ls.DbRs.AddLocalFunc("verify_create_pin", appHandlers.VerifyCreatePin)
|
||||
ls.DbRs.AddLocalFunc("check_identifier", appHandlers.CheckIdentifier)
|
||||
ls.DbRs.AddLocalFunc("check_account_status", appHandlers.CheckAccountStatus)
|
||||
ls.DbRs.AddLocalFunc("authorize_account", appHandlers.Authorize)
|
||||
ls.DbRs.AddLocalFunc("quit", appHandlers.Quit)
|
||||
ls.DbRs.AddLocalFunc("check_balance", appHandlers.CheckBalance)
|
||||
ls.DbRs.AddLocalFunc("validate_recipient", appHandlers.ValidateRecipient)
|
||||
ls.DbRs.AddLocalFunc("transaction_reset", appHandlers.TransactionReset)
|
||||
ls.DbRs.AddLocalFunc("invite_valid_recipient", appHandlers.InviteValidRecipient)
|
||||
ls.DbRs.AddLocalFunc("max_amount", appHandlers.MaxAmount)
|
||||
ls.DbRs.AddLocalFunc("validate_amount", appHandlers.ValidateAmount)
|
||||
ls.DbRs.AddLocalFunc("reset_transaction_amount", appHandlers.ResetTransactionAmount)
|
||||
ls.DbRs.AddLocalFunc("get_recipient", appHandlers.GetRecipient)
|
||||
ls.DbRs.AddLocalFunc("get_sender", appHandlers.GetSender)
|
||||
ls.DbRs.AddLocalFunc("get_amount", appHandlers.GetAmount)
|
||||
ls.DbRs.AddLocalFunc("reset_incorrect", appHandlers.ResetIncorrectPin)
|
||||
ls.DbRs.AddLocalFunc("save_firstname", appHandlers.SaveFirstname)
|
||||
ls.DbRs.AddLocalFunc("save_familyname", appHandlers.SaveFamilyname)
|
||||
ls.DbRs.AddLocalFunc("save_gender", appHandlers.SaveGender)
|
||||
ls.DbRs.AddLocalFunc("save_location", appHandlers.SaveLocation)
|
||||
ls.DbRs.AddLocalFunc("save_yob", appHandlers.SaveYob)
|
||||
ls.DbRs.AddLocalFunc("save_offerings", appHandlers.SaveOfferings)
|
||||
ls.DbRs.AddLocalFunc("reset_account_authorized", appHandlers.ResetAccountAuthorized)
|
||||
ls.DbRs.AddLocalFunc("reset_allow_update", appHandlers.ResetAllowUpdate)
|
||||
ls.DbRs.AddLocalFunc("get_profile_info", appHandlers.GetProfileInfo)
|
||||
ls.DbRs.AddLocalFunc("verify_yob", appHandlers.VerifyYob)
|
||||
ls.DbRs.AddLocalFunc("reset_incorrect_date_format", appHandlers.ResetIncorrectYob)
|
||||
ls.DbRs.AddLocalFunc("initiate_transaction", appHandlers.InitiateTransaction)
|
||||
ls.DbRs.AddLocalFunc("verify_new_pin", appHandlers.VerifyNewPin)
|
||||
ls.DbRs.AddLocalFunc("confirm_pin_change", appHandlers.ConfirmPinChange)
|
||||
ls.DbRs.AddLocalFunc("quit_with_help", appHandlers.QuitWithHelp)
|
||||
ls.DbRs.AddLocalFunc("fetch_community_balance", appHandlers.FetchCommunityBalance)
|
||||
ls.DbRs.AddLocalFunc("set_default_voucher", appHandlers.SetDefaultVoucher)
|
||||
ls.DbRs.AddLocalFunc("check_vouchers", appHandlers.CheckVouchers)
|
||||
ls.DbRs.AddLocalFunc("get_vouchers", appHandlers.GetVoucherList)
|
||||
ls.DbRs.AddLocalFunc("view_voucher", appHandlers.ViewVoucher)
|
||||
ls.DbRs.AddLocalFunc("set_voucher", appHandlers.SetVoucher)
|
||||
ls.DbRs.AddLocalFunc("get_voucher_details", appHandlers.GetVoucherDetails)
|
||||
ls.DbRs.AddLocalFunc("reset_valid_pin", appHandlers.ResetValidPin)
|
||||
ls.DbRs.AddLocalFunc("check_pin_mismatch", appHandlers.CheckBlockedNumPinMisMatch)
|
||||
ls.DbRs.AddLocalFunc("validate_blocked_number", appHandlers.ValidateBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("retrieve_blocked_number", appHandlers.RetrieveBlockedNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_unregistered_number", appHandlers.ResetUnregisteredNumber)
|
||||
ls.DbRs.AddLocalFunc("reset_others_pin", appHandlers.ResetOthersPin)
|
||||
ls.DbRs.AddLocalFunc("save_others_temporary_pin", appHandlers.SaveOthersTemporaryPin)
|
||||
ls.DbRs.AddLocalFunc("get_current_profile_info", appHandlers.GetCurrentProfileInfo)
|
||||
ls.DbRs.AddLocalFunc("check_transactions", appHandlers.CheckTransactions)
|
||||
ls.DbRs.AddLocalFunc("get_transactions", appHandlers.GetTransactionsList)
|
||||
ls.DbRs.AddLocalFunc("view_statement", appHandlers.ViewTransactionStatement)
|
||||
ls.DbRs.AddLocalFunc("update_all_profile_items", appHandlers.UpdateAllProfileItems)
|
||||
ls.DbRs.AddLocalFunc("set_back", appHandlers.SetBack)
|
||||
ls.DbRs.AddLocalFunc("show_blocked_account", appHandlers.ShowBlockedAccount)
|
||||
|
||||
return ussdHandlers, nil
|
||||
return appHandlers, nil
|
||||
}
|
||||
|
||||
// TODO: enable setting of sessionId on engine init time
|
||||
|
@ -6,9 +6,9 @@ import (
|
||||
"io"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||
)
|
||||
@ -39,7 +39,7 @@ type RequestSession struct {
|
||||
|
||||
// TODO: seems like can remove this.
|
||||
type RequestParser interface {
|
||||
GetSessionId(rq any) (string, error)
|
||||
GetSessionId(context context.Context, rq any) (string, error)
|
||||
GetInput(rq any) ([]byte, error)
|
||||
}
|
||||
|
||||
|
120
internal/http/at/parse.go
Normal file
120
internal/http/at/parse.go
Normal file
@ -0,0 +1,120 @@
|
||||
package at
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/common"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type ATRequestParser struct {
|
||||
}
|
||||
|
||||
func (arp *ATRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
logg.Warnf("got an invalid request", "req", rq)
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
// Capture body (if any) for logging
|
||||
body, err := io.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to read request body", "err", err)
|
||||
return "", fmt.Errorf("failed to read request body: %v", err)
|
||||
}
|
||||
// Reset the body for further reading
|
||||
rqv.Body = io.NopCloser(bytes.NewReader(body))
|
||||
|
||||
// Log the body as JSON
|
||||
bodyLog := map[string]string{"body": string(body)}
|
||||
logBytes, err := json.Marshal(bodyLog)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to marshal request body", "err", err)
|
||||
} else {
|
||||
decodedStr := string(logBytes)
|
||||
sessionId, err := extractATSessionId(decodedStr)
|
||||
if err != nil {
|
||||
ctx = context.WithValue(ctx, "AT-SessionId", sessionId)
|
||||
}
|
||||
logg.DebugCtxf(ctx, "Received request:", decodedStr)
|
||||
}
|
||||
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
logg.Warnf("failed to parse form data", "err", err)
|
||||
return "", fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
phoneNumber := rqv.FormValue("phoneNumber")
|
||||
if phoneNumber == "" {
|
||||
return "", fmt.Errorf("no phone number found")
|
||||
}
|
||||
|
||||
formattedNumber, err := common.FormatPhoneNumber(phoneNumber)
|
||||
if err != nil {
|
||||
logg.Warnf("failed to format phone number", "err", err)
|
||||
return "", fmt.Errorf("failed to format number")
|
||||
}
|
||||
|
||||
return formattedNumber, nil
|
||||
}
|
||||
|
||||
func (arp *ATRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
if err := rqv.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse form data: %v", err)
|
||||
}
|
||||
|
||||
text := rqv.FormValue("text")
|
||||
|
||||
parts := strings.Split(text, "*")
|
||||
if len(parts) == 0 {
|
||||
return nil, fmt.Errorf("no input found")
|
||||
}
|
||||
|
||||
trimmedInput := strings.TrimSpace(parts[len(parts)-1])
|
||||
return []byte(trimmedInput), nil
|
||||
}
|
||||
|
||||
func parseQueryParams(query string) map[string]string {
|
||||
params := make(map[string]string)
|
||||
|
||||
queryParams := strings.Split(query, "&")
|
||||
for _, param := range queryParams {
|
||||
// Split each key-value pair by '='
|
||||
parts := strings.SplitN(param, "=", 2)
|
||||
if len(parts) == 2 {
|
||||
params[parts[0]] = parts[1]
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func extractATSessionId(decodedStr string) (string, error) {
|
||||
var data map[string]string
|
||||
err := json.Unmarshal([]byte(decodedStr), &data)
|
||||
|
||||
if err != nil {
|
||||
logg.Errorf("Error unmarshalling JSON: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
decodedBody, err := url.QueryUnescape(data["body"])
|
||||
if err != nil {
|
||||
logg.Errorf("Error URL-decoding body: %v", err)
|
||||
return "", nil
|
||||
}
|
||||
params := parseQueryParams(decodedBody)
|
||||
|
||||
sessionId := params["sessionId"]
|
||||
return sessionId, nil
|
||||
|
||||
}
|
@ -1,19 +1,25 @@
|
||||
package http
|
||||
package at
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
httpserver "git.grassecon.net/urdt/ussd/internal/http"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("atserver").WithContextKey("SessionId").WithContextKey("AT-SessionId")
|
||||
)
|
||||
|
||||
type ATSessionHandler struct {
|
||||
*SessionHandler
|
||||
*httpserver.SessionHandler
|
||||
}
|
||||
|
||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||
return &ATSessionHandler{
|
||||
SessionHandler: ToSessionHandler(h),
|
||||
SessionHandler: httpserver.ToSessionHandler(h),
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,17 +34,17 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
|
||||
rp := ash.GetRequestParser()
|
||||
cfg := ash.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
ash.writeError(w, 400, err)
|
||||
ash.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
rqs, err = ash.Output(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
rqs, err = ash.Reset(rqs)
|
||||
if err != nil {
|
||||
ash.writeError(w, 500, err)
|
||||
ash.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
package at
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
@ -16,16 +15,6 @@ import (
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestNewATSessionHandler(t *testing.T) {
|
||||
mockHandler := &httpmocks.MockRequestHandler{}
|
||||
ash := NewATSessionHandler(mockHandler)
|
||||
@ -242,208 +231,4 @@ func TestATSessionHandler_Output(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_writeError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.writeError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
37
internal/http/parse.go
Normal file
37
internal/http/parse.go
Normal file
@ -0,0 +1,37 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@ -14,34 +13,6 @@ var (
|
||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
||||
)
|
||||
|
||||
type DefaultRequestParser struct {
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetSessionId(rq any) (string, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return "", handlers.ErrInvalidRequest
|
||||
}
|
||||
v := rqv.Header.Get("X-Vise-Session")
|
||||
if v == "" {
|
||||
return "", handlers.ErrSessionMissing
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (rp *DefaultRequestParser) GetInput(rq any) ([]byte, error) {
|
||||
rqv, ok := rq.(*http.Request)
|
||||
if !ok {
|
||||
return nil, handlers.ErrInvalidRequest
|
||||
}
|
||||
defer rqv.Body.Close()
|
||||
v, err := ioutil.ReadAll(rqv.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
type SessionHandler struct {
|
||||
handlers.RequestHandler
|
||||
}
|
||||
@ -52,7 +23,7 @@ func ToSessionHandler(h handlers.RequestHandler) *SessionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
func (f *SessionHandler) writeError(w http.ResponseWriter, code int, err error) {
|
||||
func (f *SessionHandler) WriteError(w http.ResponseWriter, code int, err error) {
|
||||
s := err.Error()
|
||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||
w.WriteHeader(code)
|
||||
@ -75,16 +46,16 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
rp := f.GetRequestParser()
|
||||
cfg := f.GetConfig()
|
||||
cfg.SessionId, err = rp.GetSessionId(req)
|
||||
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
f.WriteError(w, 400, err)
|
||||
}
|
||||
rqs.Config = cfg
|
||||
rqs.Input, err = rp.GetInput(req)
|
||||
if err != nil {
|
||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||
f.writeError(w, 400, err)
|
||||
f.WriteError(w, 400, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -101,7 +72,7 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
if code != 200 {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
|
||||
@ -110,11 +81,11 @@ func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
rqs, err = f.Output(rqs)
|
||||
rqs, perr = f.Reset(rqs)
|
||||
if err != nil {
|
||||
f.writeError(w, 500, err)
|
||||
f.WriteError(w, 500, err)
|
||||
return
|
||||
}
|
||||
if perr != nil {
|
||||
f.writeError(w, 500, perr)
|
||||
f.WriteError(w, 500, perr)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
230
internal/http/server_test.go
Normal file
230
internal/http/server_test.go
Normal file
@ -0,0 +1,230 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
||||
)
|
||||
|
||||
// invalidRequestType is a custom type to test invalid request scenarios
|
||||
type invalidRequestType struct{}
|
||||
|
||||
// errorReader is a helper type that always returns an error when Read is called
|
||||
type errorReader struct{}
|
||||
|
||||
func (e *errorReader) Read(p []byte) (n int, err error) {
|
||||
return 0, errors.New("read error")
|
||||
}
|
||||
|
||||
func TestSessionHandler_ServeHTTP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
sessionID string
|
||||
input []byte
|
||||
parserErr error
|
||||
processErr error
|
||||
outputErr error
|
||||
resetErr error
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Success",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
sessionID: "",
|
||||
parserErr: handlers.ErrSessionMissing,
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Process Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
processErr: handlers.ErrStorage,
|
||||
expectedStatus: http.StatusInternalServerError,
|
||||
},
|
||||
{
|
||||
name: "Output Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
outputErr: errors.New("output error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Reset Error",
|
||||
sessionID: "123",
|
||||
input: []byte("test input"),
|
||||
resetErr: errors.New("reset error"),
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
mockRequestParser := &httpmocks.MockRequestParser{
|
||||
GetSessionIdFunc: func(any) (string, error) {
|
||||
return tt.sessionID, tt.parserErr
|
||||
},
|
||||
GetInputFunc: func(any) ([]byte, error) {
|
||||
return tt.input, nil
|
||||
},
|
||||
}
|
||||
|
||||
mockRequestHandler := &httpmocks.MockRequestHandler{
|
||||
ProcessFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.processErr
|
||||
},
|
||||
OutputFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.outputErr
|
||||
},
|
||||
ResetFunc: func(rs handlers.RequestSession) (handlers.RequestSession, error) {
|
||||
return rs, tt.resetErr
|
||||
},
|
||||
GetRequestParserFunc: func() handlers.RequestParser {
|
||||
return mockRequestParser
|
||||
},
|
||||
GetConfigFunc: func() engine.Config {
|
||||
return engine.Config{}
|
||||
},
|
||||
}
|
||||
|
||||
sessionHandler := ToSessionHandler(mockRequestHandler)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(tt.input))
|
||||
req.Header.Set("X-Vise-Session", tt.sessionID)
|
||||
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
sessionHandler.ServeHTTP(rr, req)
|
||||
|
||||
if status := rr.Code; status != tt.expectedStatus {
|
||||
t.Errorf("handler returned wrong status code: got %v want %v",
|
||||
status, tt.expectedStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHandler_WriteError(t *testing.T) {
|
||||
handler := &SessionHandler{}
|
||||
mockWriter := &httpmocks.MockWriter{}
|
||||
err := errors.New("test error")
|
||||
|
||||
handler.WriteError(mockWriter, http.StatusBadRequest, err)
|
||||
|
||||
if mockWriter.WrittenString != "" {
|
||||
t.Errorf("Expected empty body, got %s", mockWriter.WrittenString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetSessionId(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedID string
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Session ID",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set("X-Vise-Session", "123456")
|
||||
return req
|
||||
}(),
|
||||
expectedID: "123456",
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Missing Session ID",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrSessionMissing,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedID: "",
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
id, err := parser.GetSessionId(context.Background(),tt.request)
|
||||
|
||||
if id != tt.expectedID {
|
||||
t.Errorf("Expected session ID %s, got %s", tt.expectedID, id)
|
||||
}
|
||||
|
||||
if err != tt.expectedError {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultRequestParser_GetInput(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
request any
|
||||
expectedInput []byte
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Valid Input",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString("test input"))
|
||||
}(),
|
||||
expectedInput: []byte("test input"),
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Empty Input",
|
||||
request: httptest.NewRequest(http.MethodPost, "/", nil),
|
||||
expectedInput: []byte{},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Invalid Request Type",
|
||||
request: invalidRequestType{},
|
||||
expectedInput: nil,
|
||||
expectedError: handlers.ErrInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "Read Error",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", &errorReader{})
|
||||
}(),
|
||||
expectedInput: nil,
|
||||
expectedError: errors.New("read error"),
|
||||
},
|
||||
}
|
||||
|
||||
parser := &DefaultRequestParser{}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
input, err := parser.GetInput(tt.request)
|
||||
|
||||
if !bytes.Equal(input, tt.expectedInput) {
|
||||
t.Errorf("Expected input %s, got %s", tt.expectedInput, input)
|
||||
}
|
||||
|
||||
if err != tt.expectedError && (err == nil || err.Error() != tt.expectedError.Error()) {
|
||||
t.Errorf("Expected error %v, got %v", tt.expectedError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
65
internal/ssh/keystore.go
Normal file
65
internal/ssh/keystore.go
Normal file
@ -0,0 +1,65 @@
|
||||
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"
|
||||
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
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 = dbstorage.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_EXTEND)
|
||||
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_EXTEND)
|
||||
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()
|
||||
}
|
284
internal/ssh/ssh.go
Normal file
284
internal/ssh/ssh.go
Normal file
@ -0,0 +1,284 @@
|
||||
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"
|
||||
"git.grassecon.net/urdt/ussd/remote"
|
||||
)
|
||||
|
||||
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) {
|
||||
logg.TraceCtxf(a.Ctx, "looking for publickey", "pubkey", fmt.Sprintf("%x", pubKey))
|
||||
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
|
||||
}
|
||||
|
||||
type SshRunner struct {
|
||||
Ctx context.Context
|
||||
Cfg engine.Config
|
||||
FlagFile string
|
||||
Conn storage.ConnData
|
||||
ResourceDir string
|
||||
Debug bool
|
||||
SrvKeyFile string
|
||||
Host string
|
||||
Port uint
|
||||
wg sync.WaitGroup
|
||||
lst net.Listener
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.Conn, s.ResourceDir)
|
||||
|
||||
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(ctx, s.FlagFile, true, dbResource, s.Cfg, rs)
|
||||
lhs.SetDataStore(&userdatastore)
|
||||
lhs.SetPersister(pe)
|
||||
lhs.Cfg.SessionId = sessionId
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// TODO: clear up why pointer here and by-value other cmds
|
||||
accountService := &remote.AccountService{}
|
||||
hl, err := lhs.GetHandler(accountService)
|
||||
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) {
|
||||
s.Ctx = ctx
|
||||
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)
|
||||
}
|
||||
}
|
@ -6,6 +6,11 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logg = logging.NewVanilla().WithDomain("gdbmstorage")
|
||||
)
|
||||
|
||||
var (
|
69
internal/storage/parse.go
Normal file
69
internal/storage/parse.go
Normal file
@ -0,0 +1,69 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
const (
|
||||
DBTYPE_MEM = iota
|
||||
DBTYPE_GDBM
|
||||
DBTYPE_POSTGRES
|
||||
)
|
||||
|
||||
type ConnData struct {
|
||||
typ int
|
||||
str string
|
||||
}
|
||||
|
||||
func (cd *ConnData) DbType() int {
|
||||
return cd.typ
|
||||
}
|
||||
|
||||
func (cd *ConnData) String() string {
|
||||
return cd.str
|
||||
}
|
||||
|
||||
func probePostgres(s string) (string, bool) {
|
||||
v, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
if v.Scheme != "postgres" {
|
||||
return "", false
|
||||
}
|
||||
return s, true
|
||||
}
|
||||
|
||||
func probeGdbm(s string) (string, bool) {
|
||||
if !path.IsAbs(s) {
|
||||
return "", false
|
||||
}
|
||||
s = path.Clean(s)
|
||||
return s, true
|
||||
}
|
||||
|
||||
func ToConnData(connStr string) (ConnData, error) {
|
||||
var o ConnData
|
||||
|
||||
if connStr == "" {
|
||||
return o, nil
|
||||
}
|
||||
|
||||
v, ok := probePostgres(connStr)
|
||||
if ok {
|
||||
o.typ = DBTYPE_POSTGRES
|
||||
o.str = v
|
||||
return o, nil
|
||||
}
|
||||
|
||||
v, ok = probeGdbm(connStr)
|
||||
if ok {
|
||||
o.typ = DBTYPE_GDBM
|
||||
o.str = v
|
||||
return o, nil
|
||||
}
|
||||
|
||||
return o, fmt.Errorf("invalid connection string: %s", connStr)
|
||||
}
|
28
internal/storage/parse_test.go
Normal file
28
internal/storage/parse_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseConnStr(t *testing.T) {
|
||||
_, err := ToConnData("postgres://foo:bar@localhost:5432/baz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = ToConnData("/foo/bar")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = ToConnData("/foo/bar/")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = ToConnData("foo/bar")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
_, err = ToConnData("http://foo/bar")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error")
|
||||
}
|
||||
}
|
@ -5,6 +5,10 @@ import (
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
)
|
||||
|
||||
const (
|
||||
DATATYPE_EXTEND = 128
|
||||
)
|
||||
|
||||
type Storage struct {
|
||||
Persister *persist.Persister
|
||||
UserdataDb db.Db
|
||||
|
@ -9,10 +9,11 @@ import (
|
||||
"git.defalsify.org/vise.git/db"
|
||||
fsdb "git.defalsify.org/vise.git/db/fs"
|
||||
"git.defalsify.org/vise.git/db/postgres"
|
||||
"git.defalsify.org/vise.git/lang"
|
||||
"git.defalsify.org/vise.git/logging"
|
||||
"git.defalsify.org/vise.git/persist"
|
||||
"git.defalsify.org/vise.git/resource"
|
||||
"git.grassecon.net/urdt/ussd/initializers"
|
||||
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -23,63 +24,49 @@ type StorageService interface {
|
||||
GetPersister(ctx context.Context) (*persist.Persister, error)
|
||||
GetUserdataDb(ctx context.Context) db.Db
|
||||
GetResource(ctx context.Context) (resource.Resource, error)
|
||||
EnsureDbDir() error
|
||||
}
|
||||
|
||||
type MenuStorageService struct {
|
||||
dbDir string
|
||||
conn ConnData
|
||||
resourceDir string
|
||||
poResource resource.Resource
|
||||
resourceStore db.Db
|
||||
stateStore db.Db
|
||||
userDataStore db.Db
|
||||
}
|
||||
|
||||
func buildConnStr() string {
|
||||
host := initializers.GetEnv("DB_HOST", "localhost")
|
||||
user := initializers.GetEnv("DB_USER", "postgres")
|
||||
password := initializers.GetEnv("DB_PASSWORD", "")
|
||||
dbName := initializers.GetEnv("DB_NAME", "")
|
||||
port := initializers.GetEnv("DB_PORT", "5432")
|
||||
|
||||
connString := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%s/%s",
|
||||
user, password, host, port, dbName,
|
||||
)
|
||||
logg.Debugf("pg conn string", "conn", connString)
|
||||
|
||||
return connString
|
||||
}
|
||||
|
||||
func NewMenuStorageService(dbDir string, resourceDir string) *MenuStorageService {
|
||||
func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService {
|
||||
return &MenuStorageService{
|
||||
dbDir: dbDir,
|
||||
conn: conn,
|
||||
resourceDir: resourceDir,
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
|
||||
database, ok := ctx.Value("Database").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to select the database")
|
||||
}
|
||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
|
||||
var newDb db.Db
|
||||
var err error
|
||||
|
||||
if existingDb != nil {
|
||||
return existingDb, nil
|
||||
}
|
||||
|
||||
var newDb db.Db
|
||||
var err error
|
||||
|
||||
if database == "postgres" {
|
||||
connStr := ms.conn.String()
|
||||
dbTyp := ms.conn.DbType()
|
||||
if dbTyp == DBTYPE_POSTGRES {
|
||||
newDb = postgres.NewPgDb()
|
||||
connStr := buildConnStr()
|
||||
err = newDb.Connect(ctx, connStr)
|
||||
} else {
|
||||
newDb = NewThreadGdbmDb()
|
||||
storeFile := path.Join(ms.dbDir, fileName)
|
||||
err = newDb.Connect(ctx, storeFile)
|
||||
} else if dbTyp == DBTYPE_GDBM {
|
||||
err = ms.ensureDbDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
connStr = path.Join(connStr, section)
|
||||
newDb = gdbmstorage.NewThreadGdbmDb()
|
||||
} else {
|
||||
return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String())
|
||||
}
|
||||
logg.DebugCtxf(ctx, "connecting to db", "conn", connStr)
|
||||
err = newDb.Connect(ctx, connStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -87,6 +74,28 @@ func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.D
|
||||
return newDb, nil
|
||||
}
|
||||
|
||||
// WithGettext triggers use of gettext for translation of templates and menus.
|
||||
//
|
||||
// The first language in `lns` will be used as default language, to resolve node keys to
|
||||
// language strings.
|
||||
//
|
||||
// If `lns` is an empty array, gettext will not be used.
|
||||
func (ms *MenuStorageService) WithGettext(path string, lns []lang.Language) *MenuStorageService {
|
||||
if len(lns) == 0 {
|
||||
logg.Warnf("Gettext requested but no languages supplied")
|
||||
return ms
|
||||
}
|
||||
rs := resource.NewPoResource(lns[0], path)
|
||||
|
||||
for _, ln := range(lns) {
|
||||
rs = rs.WithLanguage(ln)
|
||||
}
|
||||
|
||||
ms.poResource = rs
|
||||
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *MenuStorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
||||
stateStore, err := ms.GetStateStore(ctx)
|
||||
if err != nil {
|
||||
@ -119,6 +128,11 @@ func (ms *MenuStorageService) GetResource(ctx context.Context) (resource.Resourc
|
||||
return nil, err
|
||||
}
|
||||
rfs := resource.NewDbResource(ms.resourceStore)
|
||||
if ms.poResource != nil {
|
||||
logg.InfoCtxf(ctx, "using poresource for menu and template")
|
||||
rfs.WithMenuGetter(ms.poResource.GetMenu)
|
||||
rfs.WithTemplateGetter(ms.poResource.GetTemplate)
|
||||
}
|
||||
return rfs, nil
|
||||
}
|
||||
|
||||
@ -136,8 +150,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error)
|
||||
return ms.stateStore, nil
|
||||
}
|
||||
|
||||
func (ms *MenuStorageService) EnsureDbDir() error {
|
||||
err := os.MkdirAll(ms.dbDir, 0700)
|
||||
func (ms *MenuStorageService) ensureDbDir() error {
|
||||
err := os.MkdirAll(ms.conn.String(), 0700)
|
||||
if err != nil {
|
||||
return fmt.Errorf("state dir create exited with error: %v\n", err)
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"git.defalsify.org/vise.git/engine"
|
||||
@ -27,7 +28,6 @@ var (
|
||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||
ctx = context.WithValue(ctx, "Database", "gdbm")
|
||||
pfp := path.Join(scriptDir, "pp.csv")
|
||||
|
||||
var eventChannel = make(chan bool)
|
||||
@ -39,37 +39,40 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||
FlagCount: uint32(128),
|
||||
}
|
||||
|
||||
dbDir := ".test_state"
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
||||
|
||||
err := menuStorageService.EnsureDbDir()
|
||||
connStr, err := filepath.Abs(".test_state/state.gdbm")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
conn, err := storage.ToConnData(connStr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "connstr parse err: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
resourceDir := scriptDir
|
||||
menuStorageService := storage.NewMenuStorageService(conn, resourceDir)
|
||||
|
||||
rs, err := menuStorageService.GetResource(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "resource error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
pe, err := menuStorageService.GetPersister(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "persister error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "userdb error: %v", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
dbResource, ok := rs.(*resource.DbResource)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, err.Error())
|
||||
fmt.Fprintf(os.Stderr, "dbresource cast error")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
15
internal/testutil/engine_test.go
Normal file
15
internal/testutil/engine_test.go
Normal file
@ -0,0 +1,15 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCreateEngine(t *testing.T) {
|
||||
o, clean, eventC := TestEngine("foo")
|
||||
defer clean()
|
||||
defer func() {
|
||||
<-eventC
|
||||
close(eventC)
|
||||
}()
|
||||
_ = o
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
package httpmocks
|
||||
|
||||
import "context"
|
||||
|
||||
// MockRequestParser implements the handlers.RequestParser interface for testing
|
||||
type MockRequestParser struct {
|
||||
GetSessionIdFunc func(any) (string, error)
|
||||
GetInputFunc func(any) ([]byte, error)
|
||||
}
|
||||
|
||||
func (m *MockRequestParser) GetSessionId(rq any) (string, error) {
|
||||
func (m *MockRequestParser) GetSessionId(ctx context.Context, rq any) (string, error) {
|
||||
return m.GetSessionIdFunc(rq)
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ package utils
|
||||
var isoCodes = map[string]bool{
|
||||
"eng": true, // English
|
||||
"swa": true, // Swahili
|
||||
|
||||
"default": true, // Default language: English
|
||||
}
|
||||
|
||||
func IsValidISO639(code string) bool {
|
||||
|
@ -54,7 +54,7 @@
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
|
||||
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
@ -95,7 +95,7 @@
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
|
||||
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
@ -108,7 +108,6 @@
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
@ -141,7 +140,7 @@
|
||||
},
|
||||
{
|
||||
"input": "1235",
|
||||
"expectedContent": "Incorrect PIN\n1:Retry\n9:Quit"
|
||||
"expectedContent": "Incorrect PIN. You have: 2 remaining attempt(s).\n1:Retry\n9:Quit"
|
||||
},
|
||||
{
|
||||
"input": "1",
|
||||
@ -154,7 +153,6 @@
|
||||
{
|
||||
"input": "0",
|
||||
"expectedContent": "Balances:\n1:My balance\n2:Community balance\n0:Back"
|
||||
|
||||
},
|
||||
{
|
||||
"input": "0",
|
||||
@ -258,7 +256,6 @@
|
||||
"input": "0",
|
||||
"expectedContent": "{balance}\n\n1:Send\n2:My Vouchers\n3:My Account\n4:Help\n9:Quit"
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -444,9 +441,3 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
@ -17,7 +18,6 @@ import (
|
||||
|
||||
var (
|
||||
testData = driver.ReadData()
|
||||
testStore = ".test_state"
|
||||
sessionID string
|
||||
src = rand.NewSource(42)
|
||||
g = rand.New(src)
|
||||
@ -25,6 +25,11 @@ var (
|
||||
|
||||
var groupTestFile = flag.String("test-file", "group_test.json", "The test file to use for running the group tests")
|
||||
|
||||
func testStore() string {
|
||||
v, _ := filepath.Abs(".test_state/state.gdbm")
|
||||
return v
|
||||
}
|
||||
|
||||
func GenerateSessionId() string {
|
||||
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||
v, err := uu.NewV4()
|
||||
@ -81,8 +86,8 @@ func extractSendAmount(response []byte) string {
|
||||
func TestMain(m *testing.M) {
|
||||
sessionID = GenerateSessionId()
|
||||
defer func() {
|
||||
if err := os.RemoveAll(testStore); err != nil {
|
||||
log.Fatalf("Failed to delete state store %s: %v", testStore, err)
|
||||
if err := os.RemoveAll(testStore()); err != nil {
|
||||
log.Fatalf("Failed to delete state store %s: %v", testStore(), err)
|
||||
}
|
||||
}()
|
||||
m.Run()
|
||||
|
2
services/registration/blocked_account.vis
Normal file
2
services/registration/blocked_account.vis
Normal file
@ -0,0 +1,2 @@
|
||||
LOAD show_blocked_account 0
|
||||
HALT
|
@ -5,6 +5,6 @@ CATCH pin_entry flag_account_authorized 0
|
||||
MOUT english 1
|
||||
MOUT kiswahili 2
|
||||
HALT
|
||||
INCMP set_default 1
|
||||
INCMP set_eng 1
|
||||
INCMP set_swa 2
|
||||
INCMP . *
|
||||
|
@ -1 +1 @@
|
||||
Incorrect PIN
|
||||
Incorrect PIN. You have: {{.reset_incorrect}} remaining attempt(s).
|
@ -1,5 +1,7 @@
|
||||
LOAD reset_incorrect 0
|
||||
RELOAD reset_incorrect
|
||||
MAP reset_incorrect
|
||||
CATCH blocked_account flag_account_blocked 1
|
||||
MOUT retry 1
|
||||
MOUT quit 9
|
||||
HALT
|
||||
|
@ -1 +1 @@
|
||||
PIN ulioeka sio sahihi
|
||||
PIN ulioeka sio sahihi, una majaribio: {{.reset_incorrect}} yaliyobaki
|
@ -10,6 +10,9 @@ msgstr "Asante kwa kutumia huduma ya Sarafu. Kwaheri!"
|
||||
msgid "For more help, please call: 0757628885"
|
||||
msgstr "Kwa usaidizi zaidi, piga: 0757628885"
|
||||
|
||||
msgid "Your account has been locked. For help on how to unblock your account, contact support at: 0757628885"
|
||||
msgstr "Akaunti yako imefungwa. Kwa usaidizi wa jinsi ya kufungua akaunti yako, wasiliana na usaidizi kwa: 0757628885"
|
||||
|
||||
msgid "Balance: %s\n"
|
||||
msgstr "Salio: %s\n"
|
||||
|
||||
|
@ -28,3 +28,5 @@ flag,flag_gender_set,34,this is set when the gender of the profile is set
|
||||
flag,flag_location_set,35,this is set when the location of the profile is set
|
||||
flag,flag_offerings_set,36,this is set when the offerings of the profile is set
|
||||
flag,flag_back_set,37,this is set when it is a back navigation
|
||||
flag,flag_account_blocked,38,this is set when an account has been blocked after the allowed incorrect PIN attempts have been exceeded
|
||||
|
||||
|
|
@ -1,3 +1,4 @@
|
||||
CATCH blocked_account flag_account_blocked 1
|
||||
CATCH select_language flag_language_set 0
|
||||
CATCH terms flag_account_created 0
|
||||
LOAD check_account_status 0
|
||||
|
4
services/registration/set_default.vis
Normal file
4
services/registration/set_default.vis
Normal file
@ -0,0 +1,4 @@
|
||||
LOAD set_language 6
|
||||
RELOAD set_language
|
||||
CATCH terms flag_account_created 0
|
||||
MOVE language_changed
|
Loading…
Reference in New Issue
Block a user