Compare commits
84 Commits
lash/dump-
...
lash/ssh-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52fd1eced2
|
||
|
|
5c7a535288 | ||
|
|
cc2f7b41df | ||
|
|
d39740a09a
|
||
|
|
2024cc96e2
|
||
|
|
d2d878d5d7 | ||
| c995143543 | |||
|
|
44570e20ef
|
||
|
|
362eb209ef
|
||
|
|
c69d3896f1
|
||
|
|
974af6b2a7
|
||
|
|
51b6fc0dde
|
||
|
|
cc9760125a
|
||
|
|
3a9f3fa373
|
||
|
|
89c21847b9
|
||
|
|
450dfa02cc
|
||
|
|
f61e65f4fe
|
||
|
|
a4d6cef9c0
|
||
|
|
2992f7ae8e
|
||
|
|
dc61d05584
|
||
| 47b5ff0435 | |||
|
|
e92e498726 | ||
|
|
25867cf05e
|
||
|
|
c3cbe1cd92
|
||
|
|
418080d093 | ||
|
|
2e30739ec9
|
||
|
|
d5a2680500
|
||
|
|
dc1674ec55
|
||
|
|
d950b10b50
|
||
|
|
bcb3ab905e
|
||
|
|
3ed9caf16d
|
||
|
|
86464c31d2 | ||
| 5ee10d8e14 | |||
|
|
62f3681b9e
|
||
|
|
3ce1435591
|
||
|
|
f65c458daa
|
||
|
|
67007fcd48
|
||
|
|
f1b258fa6d
|
||
| d2fce05461 | |||
|
|
68ac237449 | ||
|
|
162e6c1934
|
||
| 8bd025f2b2 | |||
|
|
9d6e25e184
|
||
|
|
c26f5683f6
|
||
|
91dc9ce82f
|
|||
|
|
0fe48a30fa
|
||
| 58edfa01a2 | |||
|
|
3830c12a57
|
||
|
|
f1fd690a7b
|
||
|
|
491b7424a9
|
||
|
|
29ce4b83bd
|
||
|
|
ca8df5989a
|
||
|
|
82b4365d16
|
||
|
|
98db85511b
|
||
|
|
99a4d3ff42
|
||
|
|
d95c7abea4
|
||
|
|
fd1ac85a1b
|
||
|
|
c899c098f6
|
||
|
|
5ca6a74274
|
||
|
|
48d63fb43f
|
||
|
|
e666c58644
|
||
|
|
e980586910
|
||
|
|
ffd5be1f1f
|
||
| ed1aeecf7d | |||
| 3b69f3d38d | |||
| 7c4c73125e | |||
|
|
4ea52bf3fb
|
||
|
|
be2ea3a2f0
|
||
|
|
8217ea8fdc | ||
|
|
3c73fc7188
|
||
|
|
1311a0cab9
|
||
|
|
50c006546c
|
||
|
|
e8c171a82e | ||
|
|
be215d3f75
|
||
|
|
dfd0a0994b | ||
|
|
5534706189
|
||
|
|
5428626c3f
|
||
|
|
f8ea2daa73
|
||
|
|
5d8de80a18
|
||
|
|
bb1a846cb3 | ||
|
|
967e53d83b | ||
|
|
d246cdee51
|
||
|
|
d518a76536 | ||
|
|
6f65c33be4
|
10
.env.example
10
.env.example
@@ -6,13 +6,9 @@ HOST=127.0.0.1
|
|||||||
AT_ENDPOINT=/ussd/africastalking
|
AT_ENDPOINT=/ussd/africastalking
|
||||||
|
|
||||||
#PostgreSQL
|
#PostgreSQL
|
||||||
DB_HOST=localhost
|
DB_CONN=postgres://postgres:strongpass@localhost:5432/urdt_ussd
|
||||||
DB_USER=postgres
|
#DB_TIMEZONE=Africa/Nairobi
|
||||||
DB_PASSWORD=strongpass
|
#DB_SCHEMA=vise
|
||||||
DB_NAME=urdt_ussd
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_SSLMODE=disable
|
|
||||||
DB_TIMEZONE=Africa/Nairobi
|
|
||||||
|
|
||||||
#External API Calls
|
#External API Calls
|
||||||
CUSTODIAL_URL_BASE=http://localhost:5003
|
CUSTODIAL_URL_BASE=http://localhost:5003
|
||||||
|
|||||||
@@ -1,35 +1,30 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/common"
|
|
||||||
"git.grassecon.net/urdt/ussd/config"
|
"git.grassecon.net/urdt/ussd/config"
|
||||||
"git.grassecon.net/urdt/ussd/initializers"
|
"git.grassecon.net/urdt/ussd/initializers"
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"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/internal/storage"
|
||||||
"git.grassecon.net/urdt/ussd/remote"
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla().WithDomain("AfricasTalking").WithContextKey("at-session-id")
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
build = "dev"
|
build = "dev"
|
||||||
menuSeparator = ": "
|
menuSeparator = ": "
|
||||||
@@ -39,91 +34,36 @@ func init() {
|
|||||||
initializers.LoadEnvVariables()
|
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() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
var dbDir string
|
var connStr string
|
||||||
var resourceDir string
|
var resourceDir string
|
||||||
var size uint
|
var size uint
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
var host string
|
var host string
|
||||||
var port uint
|
var port uint
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
var err error
|
||||||
|
|
||||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
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.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logg.Infof("start command", "build", build, "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
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", "build", build, "conn", connData, "resourcedir", resourceDir, "outputsize", size)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "Database", database)
|
ctx = context.WithValue(ctx, "Database", database)
|
||||||
@@ -140,14 +80,13 @@ func main() {
|
|||||||
cfg.EngineDebug = true
|
cfg.EngineDebug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = menuStorageService.EnsureDbDir()
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -191,9 +130,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer stateStore.Close()
|
defer stateStore.Close()
|
||||||
|
|
||||||
rp := &atRequestParser{}
|
rp := &at.ATRequestParser{}
|
||||||
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
bsh := handlers.NewBaseSessionHandler(cfg, rs, stateStore, userdataStore, rp, hl)
|
||||||
sh := httpserver.NewATSessionHandler(bsh)
|
sh := at.NewATSessionHandler(bsh)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
|
mux.Handle(initializers.GetEnv("AT_ENDPOINT", "/"), sh)
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla()
|
logg = logging.NewVanilla()
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
menuSeparator = ": "
|
menuSeparator = ": "
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ type asyncRequestParser struct {
|
|||||||
input []byte
|
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
|
return p.sessionId, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,25 +46,35 @@ func (p *asyncRequestParser) GetInput(r any) ([]byte, error) {
|
|||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
var sessionId string
|
var sessionId string
|
||||||
var dbDir string
|
|
||||||
var resourceDir string
|
var resourceDir string
|
||||||
var size uint
|
var size uint
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
var host string
|
var host string
|
||||||
var port uint
|
var port uint
|
||||||
|
var err error
|
||||||
|
|
||||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
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(&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.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
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, "resourcedir", resourceDir, "outputsize", size, "sessionId", sessionId)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "Database", database)
|
ctx = context.WithValue(ctx, "Database", database)
|
||||||
@@ -81,18 +91,18 @@ func main() {
|
|||||||
cfg.EngineDebug = true
|
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)
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -36,23 +36,33 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
var dbDir string
|
var connStr string
|
||||||
var resourceDir string
|
var resourceDir string
|
||||||
var size uint
|
var size uint
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
var host string
|
var host string
|
||||||
var port uint
|
var port uint
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
var err error
|
||||||
|
|
||||||
flag.StringVar(&resourceDir, "resourcedir", path.Join("services", "registration"), "resource dir")
|
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.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
flag.StringVar(&host, "h", initializers.GetEnv("HOST", "127.0.0.1"), "http host")
|
||||||
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
flag.UintVar(&port, "p", initializers.GetEnvUint("PORT", 7123), "http port")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size)
|
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, "resourcedir", resourceDir, "outputsize", size)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "Database", database)
|
ctx = context.WithValue(ctx, "Database", database)
|
||||||
@@ -69,19 +79,14 @@ func main() {
|
|||||||
cfg.EngineDebug = true
|
cfg.EngineDebug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
userdataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
|||||||
29
cmd/main.go
29
cmd/main.go
@@ -30,19 +30,31 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
var dbDir string
|
var connStr string
|
||||||
var size uint
|
var size uint
|
||||||
var sessionId string
|
var sessionId string
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
|
var resourceDir string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
flag.StringVar(&resourceDir, "resourcedir", scriptDir, "resource dir")
|
||||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
|
||||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.UintVar(&size, "s", 160, "max size of output")
|
flag.UintVar(&size, "s", 160, "max size of output")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
logg.Infof("start command", "dbdir", dbDir, "outputsize", size)
|
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, "outputsize", size)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
@@ -57,14 +69,7 @@ func main() {
|
|||||||
MenuSeparator: menuSeparator,
|
MenuSeparator: menuSeparator,
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceDir := scriptDir
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
|
||||||
|
|
||||||
err := menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
rs, err := menuStorageService.GetResource(ctx)
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
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>
|
||||||
|
```
|
||||||
133
cmd/ssh/main.go
Normal file
133
cmd/ssh/main.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/db"
|
||||||
|
"git.defalsify.org/vise.git/engine"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
|
||||||
|
"git.grassecon.net/urdt/ussd/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")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
initializers.LoadEnvVariables()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
config.LoadConfig()
|
||||||
|
|
||||||
|
var connStr string
|
||||||
|
var dbDir 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(&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
|
||||||
|
}
|
||||||
|
connData, err := storage.ToConnData(config.DbConn)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "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", "dbdir", dbDir, "resourcedir", resourceDir, "outputsize", size, "keyfile", sshKeyFile, "host", host, "port", port)
|
||||||
|
|
||||||
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
|
cfg := engine.Config{
|
||||||
|
Root: "root",
|
||||||
|
OutputSize: uint32(size),
|
||||||
|
FlagCount: uint32(16),
|
||||||
|
}
|
||||||
|
if stateDebug {
|
||||||
|
cfg.StateDebug = true
|
||||||
|
}
|
||||||
|
if engineDebug {
|
||||||
|
cfg.EngineDebug = true
|
||||||
|
}
|
||||||
|
|
||||||
|
authKeyStore, err := ssh.NewSshKeyStore(ctx, dbDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "keystore file open error: %v", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer func () {
|
||||||
|
logg.TraceCtxf(ctx, "shutdown auth key store reached")
|
||||||
|
err = authKeyStore.Close()
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "keystore close error", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cint := make(chan os.Signal)
|
||||||
|
cterm := make(chan os.Signal)
|
||||||
|
signal.Notify(cint, os.Interrupt, syscall.SIGINT)
|
||||||
|
signal.Notify(cterm, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
|
runner := &ssh.SshRunner{
|
||||||
|
Cfg: cfg,
|
||||||
|
Debug: engineDebug,
|
||||||
|
FlagFile: pfp,
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
33
common/pin.go
Normal file
33
common/pin.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Define the regex pattern as a constant
|
||||||
|
const (
|
||||||
|
pinPattern = `^\d{4}$`
|
||||||
|
)
|
||||||
|
|
||||||
|
// checks whether the given input is a 4 digit number
|
||||||
|
func IsValidPIN(pin string) bool {
|
||||||
|
match, _ := regexp.MatchString(pinPattern, pin)
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPIN uses bcrypt with 8 salt rounds to hash the PIN
|
||||||
|
func HashPIN(pin string) (string, error) {
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(pin), 8)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(hash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPIN compareS the hashed PIN with the plaintext PIN
|
||||||
|
func VerifyPIN(hashedPIN, pin string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(pin))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
173
common/pin_test.go
Normal file
173
common/pin_test.go
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsValidPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN with 4 digits",
|
||||||
|
pin: "1234",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid PIN with leading zeros",
|
||||||
|
pin: "0001",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with less than 4 digits",
|
||||||
|
pin: "123",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with more than 4 digits",
|
||||||
|
pin: "12345",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with letters",
|
||||||
|
pin: "abcd",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN with special characters",
|
||||||
|
pin: "12@#",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty PIN",
|
||||||
|
pin: "",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
actual := IsValidPIN(tt.pin)
|
||||||
|
if actual != tt.expected {
|
||||||
|
t.Errorf("IsValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHashPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN with 4 digits",
|
||||||
|
pin: "1234",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Valid PIN with leading zeros",
|
||||||
|
pin: "0001",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Empty PIN",
|
||||||
|
pin: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
hashedPIN, err := HashPIN(tt.pin)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("HashPIN(%q) returned an error: %v", tt.pin, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hashedPIN == "" {
|
||||||
|
t.Errorf("HashPIN(%q) returned an empty hash", tt.pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the hash can be verified with bcrypt
|
||||||
|
err = bcrypt.CompareHashAndPassword([]byte(hashedPIN), []byte(tt.pin))
|
||||||
|
if tt.pin != "" && err != nil {
|
||||||
|
t.Errorf("HashPIN(%q) produced a hash that does not match: %v", tt.pin, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyMigratedHashPin(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
pin string
|
||||||
|
hash string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pin: "1234",
|
||||||
|
hash: "$2b$08$dTvIGxCCysJtdvrSnaLStuylPoOS/ZLYYkxvTeR5QmTFY3TSvPQC6",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.pin, func(t *testing.T) {
|
||||||
|
ok := VerifyPIN(tt.hash, tt.pin)
|
||||||
|
if !ok {
|
||||||
|
t.Errorf("VerifyPIN could not verify migrated PIN: %v", tt.pin)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVerifyPIN(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
pin string
|
||||||
|
hashedPIN string
|
||||||
|
shouldPass bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Valid PIN verification",
|
||||||
|
pin: "1234",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with incorrect PIN",
|
||||||
|
pin: "5678",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with empty PIN",
|
||||||
|
pin: "",
|
||||||
|
hashedPIN: hashPINHelper("1234"),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Invalid PIN verification with invalid hash",
|
||||||
|
pin: "1234",
|
||||||
|
hashedPIN: "invalidhash",
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := VerifyPIN(tt.hashedPIN, tt.pin)
|
||||||
|
if result != tt.shouldPass {
|
||||||
|
t.Errorf("VerifyPIN(%q, %q) = %v; expected %v", tt.hashedPIN, tt.pin, result, tt.shouldPass)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to hash a PIN for testing purposes
|
||||||
|
func hashPINHelper(pin string) string {
|
||||||
|
hashedPIN, err := HashPIN(pin)
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to hash PIN for test setup: " + err.Error())
|
||||||
|
}
|
||||||
|
return hashedPIN
|
||||||
|
}
|
||||||
@@ -8,31 +8,32 @@ import (
|
|||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
|
dbstorage "git.grassecon.net/urdt/ussd/internal/storage/db"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StoreToDb(store *UserDataStore) db.Db {
|
func StoreToDb(store *UserDataStore) db.Db {
|
||||||
return store.Db
|
return store.Db
|
||||||
}
|
}
|
||||||
|
|
||||||
func StoreToPrefixDb(store *UserDataStore, pfx []byte) storage.PrefixDb {
|
func StoreToPrefixDb(store *UserDataStore, pfx []byte) dbstorage.PrefixDb {
|
||||||
return storage.NewSubPrefixDb(store.Db, pfx)
|
return dbstorage.NewSubPrefixDb(store.Db, pfx)
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageServices interface {
|
type StorageServices interface {
|
||||||
GetPersister(ctx context.Context) (*persist.Persister, error)
|
GetPersister(ctx context.Context) (*persist.Persister, error)
|
||||||
GetUserdataDb(ctx context.Context) (db.Db, error)
|
GetUserdataDb(ctx context.Context) (db.Db, error)
|
||||||
GetResource(ctx context.Context) (resource.Resource, error)
|
GetResource(ctx context.Context) (resource.Resource, error)
|
||||||
EnsureDbDir() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type StorageService struct {
|
type StorageService struct {
|
||||||
svc *storage.MenuStorageService
|
svc *storage.MenuStorageService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStorageService(dbDir string) *StorageService {
|
func NewStorageService(conn storage.ConnData) (*StorageService, error) {
|
||||||
return &StorageService{
|
svc := &StorageService{
|
||||||
svc: storage.NewMenuStorageService(dbDir, ""),
|
svc: storage.NewMenuStorageService(conn, ""),
|
||||||
}
|
}
|
||||||
|
return svc, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func(ss *StorageService) GetPersister(ctx context.Context) (*persist.Persister, error) {
|
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) {
|
func(ss *StorageService) GetResource(ctx context.Context) (resource.Resource, error) {
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func(ss *StorageService) EnsureDbDir() error {
|
|
||||||
return ss.svc.EnsureDbDir()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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"
|
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
|
// GetTransferData retrieves and matches transfer data
|
||||||
// returns a formatted string of the full transaction/statement
|
// 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}
|
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)
|
data := make(map[DataTyp]string)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"math/big"
|
"math/big"
|
||||||
"strings"
|
"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"
|
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
|
// 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}
|
keys := []DataTyp{DATA_VOUCHER_SYMBOLS, DATA_VOUCHER_BALANCES, DATA_VOUCHER_DECIMALS, DATA_VOUCHER_ADDRESSES}
|
||||||
data := make(map[DataTyp]string)
|
data := make(map[DataTyp]string)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
visedb "git.defalsify.org/vise.git/db"
|
visedb "git.defalsify.org/vise.git/db"
|
||||||
memdb "git.defalsify.org/vise.git/db/mem"
|
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"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ func TestGetVoucherData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
prefix := ToBytes(visedb.DATATYPE_USERDATA)
|
||||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||||
|
|
||||||
// Test voucher data
|
// Test voucher data
|
||||||
mockData := map[DataTyp][]byte{
|
mockData := map[DataTyp][]byte{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ var (
|
|||||||
VoucherTransfersURL string
|
VoucherTransfersURL string
|
||||||
VoucherDataURL string
|
VoucherDataURL string
|
||||||
CheckAliasURL string
|
CheckAliasURL string
|
||||||
|
DbConn string
|
||||||
)
|
)
|
||||||
|
|
||||||
func setBase() error {
|
func setBase() error {
|
||||||
@@ -43,14 +44,20 @@ func setBase() error {
|
|||||||
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
|
dataURLBase = initializers.GetEnv("DATA_URL_BASE", "http://localhost:5006")
|
||||||
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
|
BearerToken = initializers.GetEnv("BEARER_TOKEN", "")
|
||||||
|
|
||||||
_, err = url.JoinPath(custodialURLBase, "/foo")
|
_, err = url.Parse(custodialURLBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = url.JoinPath(dataURLBase, "/bar")
|
_, err = url.Parse(dataURLBase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setConn() error {
|
||||||
|
DbConn = initializers.GetEnv("DB_CONN", "")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,6 +67,10 @@ func LoadConfig() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = setConn()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
CreateAccountURL, _ = url.JoinPath(custodialURLBase, createAccountPath)
|
||||||
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
TrackStatusURL, _ = url.JoinPath(custodialURLBase, trackStatusPath)
|
||||||
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
BalanceURL, _ = url.JoinPath(custodialURLBase, balancePathPrefix)
|
||||||
|
|||||||
@@ -28,24 +28,35 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
var dbDir string
|
var connStr string
|
||||||
var sessionId string
|
var sessionId string
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
|
var err error
|
||||||
|
|
||||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
flag.StringVar(&connStr, "c", "", "connection string")
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
|
||||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.Parse()
|
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.Background()
|
||||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
ctx = context.WithValue(ctx, "Database", database)
|
ctx = context.WithValue(ctx, "Database", database)
|
||||||
|
|
||||||
resourceDir := scriptDir
|
resourceDir := scriptDir
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
|
||||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, err.Error())
|
||||||
|
|||||||
@@ -28,23 +28,34 @@ func init() {
|
|||||||
func main() {
|
func main() {
|
||||||
config.LoadConfig()
|
config.LoadConfig()
|
||||||
|
|
||||||
var dbDir string
|
var connStr string
|
||||||
var sessionId string
|
var sessionId string
|
||||||
var database string
|
var database string
|
||||||
var engineDebug bool
|
var engineDebug bool
|
||||||
|
var err error
|
||||||
|
|
||||||
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
flag.StringVar(&sessionId, "session-id", "075xx2123", "session id")
|
||||||
flag.StringVar(&database, "db", "gdbm", "database to be used")
|
flag.StringVar(&connStr, "c", ".state", "connection string")
|
||||||
flag.StringVar(&dbDir, "dbdir", ".state", "database dir to read from")
|
|
||||||
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
flag.BoolVar(&engineDebug, "d", false, "use engine debug output")
|
||||||
flag.Parse()
|
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.Background()
|
||||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
ctx = context.WithValue(ctx, "Database", database)
|
ctx = context.WithValue(ctx, "Database", database)
|
||||||
|
|
||||||
resourceDir := scriptDir
|
resourceDir := scriptDir
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
menuStorageService := storage.NewMenuStorageService(connData, resourceDir)
|
||||||
|
|
||||||
store, err := menuStorageService.GetUserdataDb(ctx)
|
store, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
4
go.mod
4
go.mod
@@ -3,7 +3,7 @@ module git.grassecon.net/urdt/ussd
|
|||||||
go 1.23.0
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
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/alecthomas/assert/v2 v2.2.2
|
||||||
github.com/gofrs/uuid v4.4.0+incompatible
|
github.com/gofrs/uuid v4.4.0+incompatible
|
||||||
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
github.com/grassrootseconomics/eth-custodial v1.3.0-beta
|
||||||
@@ -11,6 +11,7 @@ require (
|
|||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/peteole/testdata-loader v0.3.0
|
github.com/peteole/testdata-loader v0.3.0
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/stretchr/testify v1.9.0
|
||||||
|
golang.org/x/crypto v0.27.0
|
||||||
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
gopkg.in/leonelquinteros/gotext.v1 v1.3.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +33,6 @@ require (
|
|||||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.2 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // 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/sync v0.8.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.18.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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.20250103172917-3e190a44568d h1:bPAOVZOX4frSGhfOdcj7kc555f8dc9DmMd2YAyC2AMw=
|
||||||
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/go.mod h1:jyBMe1qTYUz3mmuoC9JQ/TvFeW0vTanCUcPu3H8p4Ck=
|
||||||
github.com/alecthomas/assert/v2 v2.2.2 h1:Z/iVC0xZfWTaFNE6bA3z07T86hd45Xe2eLt6WVy2bbk=
|
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/assert/v2 v2.2.2/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ=
|
||||||
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
github.com/alecthomas/participle/v2 v2.0.0 h1:Fgrq+MbuSsJwIkw3fEj9h75vDP0Er5JzepJ0/HNHv0g=
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
"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/logging"
|
||||||
|
"git.defalsify.org/vise.git/persist"
|
||||||
|
"git.defalsify.org/vise.git/resource"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/internal/storage"
|
"git.grassecon.net/urdt/ussd/internal/storage"
|
||||||
)
|
)
|
||||||
@@ -20,33 +20,33 @@ var (
|
|||||||
var (
|
var (
|
||||||
ErrInvalidRequest = errors.New("invalid request for context")
|
ErrInvalidRequest = errors.New("invalid request for context")
|
||||||
ErrSessionMissing = errors.New("missing session")
|
ErrSessionMissing = errors.New("missing session")
|
||||||
ErrInvalidInput = errors.New("invalid input")
|
ErrInvalidInput = errors.New("invalid input")
|
||||||
ErrStorage = errors.New("storage retrieval fail")
|
ErrStorage = errors.New("storage retrieval fail")
|
||||||
ErrEngineType = errors.New("incompatible engine")
|
ErrEngineType = errors.New("incompatible engine")
|
||||||
ErrEngineInit = errors.New("engine init fail")
|
ErrEngineInit = errors.New("engine init fail")
|
||||||
ErrEngineExec = errors.New("engine exec fail")
|
ErrEngineExec = errors.New("engine exec fail")
|
||||||
)
|
)
|
||||||
|
|
||||||
type RequestSession struct {
|
type RequestSession struct {
|
||||||
Ctx context.Context
|
Ctx context.Context
|
||||||
Config engine.Config
|
Config engine.Config
|
||||||
Engine engine.Engine
|
Engine engine.Engine
|
||||||
Input []byte
|
Input []byte
|
||||||
Storage *storage.Storage
|
Storage *storage.Storage
|
||||||
Writer io.Writer
|
Writer io.Writer
|
||||||
Continue bool
|
Continue bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: seems like can remove this.
|
// TODO: seems like can remove this.
|
||||||
type RequestParser interface {
|
type RequestParser interface {
|
||||||
GetSessionId(rq any) (string, error)
|
GetSessionId(context context.Context, rq any) (string, error)
|
||||||
GetInput(rq any) ([]byte, error)
|
GetInput(rq any) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type RequestHandler interface {
|
type RequestHandler interface {
|
||||||
GetConfig() engine.Config
|
GetConfig() engine.Config
|
||||||
GetRequestParser() RequestParser
|
GetRequestParser() RequestParser
|
||||||
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
GetEngine(cfg engine.Config, rs resource.Resource, pe *persist.Persister) engine.Engine
|
||||||
Process(rs RequestSession) (RequestSession, error)
|
Process(rs RequestSession) (RequestSession, error)
|
||||||
Output(rs RequestSession) (RequestSession, error)
|
Output(rs RequestSession) (RequestSession, error)
|
||||||
Reset(rs RequestSession) (RequestSession, error)
|
Reset(rs RequestSession) (RequestSession, error)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -24,27 +23,16 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/remote"
|
"git.grassecon.net/urdt/ussd/remote"
|
||||||
"gopkg.in/leonelquinteros/gotext.v1"
|
"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"
|
dataserviceapi "github.com/grassrootseconomics/ussd-data-service/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
logg = logging.NewVanilla().WithDomain("ussdmenuhandler")
|
logg = logging.NewVanilla().WithDomain("ussdmenuhandler").WithContextKey("SessionId")
|
||||||
scriptDir = path.Join("services", "registration")
|
scriptDir = path.Join("services", "registration")
|
||||||
translationDir = path.Join(scriptDir, "locale")
|
translationDir = path.Join(scriptDir, "locale")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define the regex patterns as constants
|
|
||||||
const (
|
|
||||||
pinPattern = `^\d{4}$`
|
|
||||||
)
|
|
||||||
|
|
||||||
// checks whether the given input is a 4 digit number
|
|
||||||
func isValidPIN(pin string) bool {
|
|
||||||
match, _ := regexp.MatchString(pinPattern, pin)
|
|
||||||
return match
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlagManager handles centralized flag management
|
// FlagManager handles centralized flag management
|
||||||
type FlagManager struct {
|
type FlagManager struct {
|
||||||
parser *asm.FlagParser
|
parser *asm.FlagParser
|
||||||
@@ -76,7 +64,7 @@ type Handlers struct {
|
|||||||
adminstore *utils.AdminStore
|
adminstore *utils.AdminStore
|
||||||
flagManager *asm.FlagParser
|
flagManager *asm.FlagParser
|
||||||
accountService remote.AccountServiceInterface
|
accountService remote.AccountServiceInterface
|
||||||
prefixDb storage.PrefixDb
|
prefixDb dbstorage.PrefixDb
|
||||||
profile *models.Profile
|
profile *models.Profile
|
||||||
ReplaceSeparatorFunc func(string) string
|
ReplaceSeparatorFunc func(string) string
|
||||||
}
|
}
|
||||||
@@ -92,7 +80,7 @@ func NewHandlers(appFlags *asm.FlagParser, userdataStore db.Db, adminstore *util
|
|||||||
|
|
||||||
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
// Instantiate the SubPrefixDb with "DATATYPE_USERDATA" prefix
|
||||||
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
prefix := common.ToBytes(db.DATATYPE_USERDATA)
|
||||||
prefixDb := storage.NewSubPrefixDb(userdataStore, prefix)
|
prefixDb := dbstorage.NewSubPrefixDb(userdataStore, prefix)
|
||||||
|
|
||||||
h := &Handlers{
|
h := &Handlers{
|
||||||
userdataStore: userDb,
|
userdataStore: userDb,
|
||||||
@@ -129,9 +117,17 @@ func (h *Handlers) Init(ctx context.Context, sym string, input []byte) (resource
|
|||||||
h.st = h.pe.GetState()
|
h.st = h.pe.GetState()
|
||||||
h.ca = h.pe.GetMemory()
|
h.ca = h.pe.GetMemory()
|
||||||
|
|
||||||
sessionId, _ := ctx.Value("SessionId").(string)
|
if len(input) == 0 {
|
||||||
flag_admin_privilege, _ := h.flagManager.GetFlag("flag_admin_privilege")
|
// move to the top node
|
||||||
|
h.st.Code = []byte{}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
isAdmin, _ := h.adminstore.IsAdmin(sessionId)
|
||||||
|
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
@@ -276,7 +272,7 @@ func (h *Handlers) VerifyNewPin(ctx context.Context, sym string, input []byte) (
|
|||||||
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
flag_valid_pin, _ := h.flagManager.GetFlag("flag_valid_pin")
|
||||||
pinInput := string(input)
|
pinInput := string(input)
|
||||||
// Validate that the PIN is a 4-digit number.
|
// Validate that the PIN is a 4-digit number.
|
||||||
if isValidPIN(pinInput) {
|
if common.IsValidPIN(pinInput) {
|
||||||
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
res.FlagSet = append(res.FlagSet, flag_valid_pin)
|
||||||
} else {
|
} else {
|
||||||
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
res.FlagReset = append(res.FlagReset, flag_valid_pin)
|
||||||
@@ -301,7 +297,7 @@ func (h *Handlers) SaveTemporaryPin(ctx context.Context, sym string, input []byt
|
|||||||
accountPIN := string(input)
|
accountPIN := string(input)
|
||||||
|
|
||||||
// Validate that the PIN is a 4-digit number.
|
// Validate that the PIN is a 4-digit number.
|
||||||
if !isValidPIN(accountPIN) {
|
if !common.IsValidPIN(accountPIN) {
|
||||||
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
res.FlagSet = append(res.FlagSet, flag_incorrect_pin)
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@@ -363,11 +359,20 @@ func (h *Handlers) ConfirmPinChange(ctx context.Context, sym string, input []byt
|
|||||||
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
res.FlagReset = append(res.FlagReset, flag_pin_mismatch)
|
||||||
} else {
|
} else {
|
||||||
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
res.FlagSet = append(res.FlagSet, flag_pin_mismatch)
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
// If matched, save the confirmed PIN as the new account PIN
|
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the hashed PIN as the new account PIN
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "hashedPIN value", hashedPIN, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
return res, nil
|
return res, nil
|
||||||
@@ -399,11 +404,19 @@ func (h *Handlers) VerifyCreatePin(ctx context.Context, sym string, input []byte
|
|||||||
res.FlagSet = append(res.FlagSet, flag_pin_set)
|
res.FlagSet = append(res.FlagSet, flag_pin_set)
|
||||||
} else {
|
} else {
|
||||||
res.FlagSet = []uint32{flag_pin_mismatch}
|
res.FlagSet = []uint32{flag_pin_mismatch}
|
||||||
|
return res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(ctx, "failed to write temporaryPin entry with", "key", common.DATA_ACCOUNT_PIN, "value", temporaryPin, "error", err)
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to write DATA_ACCOUNT_PIN entry with", "key", common.DATA_ACCOUNT_PIN, "value", hashedPIN, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,7 +730,7 @@ func (h *Handlers) Authorize(ctx context.Context, sym string, input []byte) (res
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
if len(input) == 4 {
|
if len(input) == 4 {
|
||||||
if bytes.Equal(input, AccountPin) {
|
if common.VerifyPIN(string(AccountPin), string(input)) {
|
||||||
if h.st.MatchFlag(flag_account_authorized, false) {
|
if h.st.MatchFlag(flag_account_authorized, false) {
|
||||||
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
res.FlagReset = append(res.FlagReset, flag_incorrect_pin)
|
||||||
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
|
res.FlagSet = append(res.FlagSet, flag_allow_update, flag_account_authorized)
|
||||||
@@ -944,7 +957,15 @@ func (h *Handlers) ResetOthersPin(ctx context.Context, sym string, input []byte)
|
|||||||
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
logg.ErrorCtxf(ctx, "failed to read temporaryPin entry with", "key", common.DATA_TEMPORARY_VALUE, "error", err)
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(temporaryPin))
|
|
||||||
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(string(temporaryPin))
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, string(blockedPhonenumber), common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, nil
|
return res, nil
|
||||||
}
|
}
|
||||||
@@ -1395,7 +1416,6 @@ func (h *Handlers) GetCurrentProfileInfo(ctx context.Context, sym string, input
|
|||||||
defaultValue = "Not Provided"
|
defaultValue = "Not Provided"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
sm, _ := h.st.Where()
|
sm, _ := h.st.Where()
|
||||||
parts := strings.SplitN(sm, "_", 2)
|
parts := strings.SplitN(sm, "_", 2)
|
||||||
filename := parts[1]
|
filename := parts[1]
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/cache"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.defalsify.org/vise.git/lang"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.defalsify.org/vise.git/state"
|
"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/mocks"
|
||||||
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
"git.grassecon.net/urdt/ussd/internal/testutil/testservice"
|
||||||
|
"git.grassecon.net/urdt/ussd/internal/utils"
|
||||||
"git.grassecon.net/urdt/ussd/models"
|
"git.grassecon.net/urdt/ussd/models"
|
||||||
|
|
||||||
"git.grassecon.net/urdt/ussd/common"
|
"git.grassecon.net/urdt/ussd/common"
|
||||||
@@ -57,14 +59,14 @@ func InitializeTestStore(t *testing.T) (context.Context, *common.UserDataStore)
|
|||||||
return ctx, store
|
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()
|
db := memdb.NewMemDb()
|
||||||
err := db.Connect(ctx, "")
|
err := db.Connect(ctx, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
prefix := common.ToBytes(visedb.DATATYPE_USERDATA)
|
||||||
spdb := storage.NewSubPrefixDb(db, prefix)
|
spdb := dbstorage.NewSubPrefixDb(db, prefix)
|
||||||
|
|
||||||
return spdb
|
return spdb
|
||||||
}
|
}
|
||||||
@@ -119,6 +121,102 @@ func TestNewHandlers(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInit(t *testing.T) {
|
||||||
|
sessionId := "session123"
|
||||||
|
ctx, store := InitializeTestStore(t)
|
||||||
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
|
||||||
|
fm, err := NewFlagManager(flagsPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
adminstore, err := utils.NewAdminStore(ctx, "admin_numbers")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
st := state.NewState(128)
|
||||||
|
ca := cache.NewCache()
|
||||||
|
|
||||||
|
flag_admin_privilege, _ := fm.GetFlag("flag_admin_privilege")
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() (*Handlers, context.Context)
|
||||||
|
input []byte
|
||||||
|
expectedResult resource.Result
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Handler not ready",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
return &Handlers{}, ctx
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "State and memory initialization",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Non-admin session initialization",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession("0712345678").WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
return h, context.WithValue(context.Background(), "SessionId", "0712345678")
|
||||||
|
},
|
||||||
|
input: []byte("1"),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Move to top node on empty input",
|
||||||
|
setup: func() (*Handlers, context.Context) {
|
||||||
|
pe := persist.NewPersister(store).WithSession(sessionId).WithContent(st, ca)
|
||||||
|
h := &Handlers{
|
||||||
|
flagManager: fm.parser,
|
||||||
|
adminstore: adminstore,
|
||||||
|
pe: pe,
|
||||||
|
}
|
||||||
|
st.Code = []byte("some pending bytecode")
|
||||||
|
return h, context.WithValue(ctx, "SessionId", sessionId)
|
||||||
|
},
|
||||||
|
input: []byte(""),
|
||||||
|
expectedResult: resource.Result{
|
||||||
|
FlagReset: []uint32{flag_admin_privilege},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
h, testCtx := tt.setup()
|
||||||
|
res, err := h.Init(testCtx, "", tt.input)
|
||||||
|
|
||||||
|
assert.NoError(t, err, "Unexpected error occurred")
|
||||||
|
assert.Equal(t, res, tt.expectedResult, "Expected result should match actual result")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateAccount(t *testing.T) {
|
func TestCreateAccount(t *testing.T) {
|
||||||
sessionId := "session123"
|
sessionId := "session123"
|
||||||
ctx, store := InitializeTestStore(t)
|
ctx, store := InitializeTestStore(t)
|
||||||
@@ -949,7 +1047,14 @@ func TestAuthorize(t *testing.T) {
|
|||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(accountPIN))
|
// Hash the PIN
|
||||||
|
hashedPIN, err := common.HashPIN(accountPIN)
|
||||||
|
if err != nil {
|
||||||
|
logg.ErrorCtxf(ctx, "failed to hash temporaryPin", "error", err)
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.WriteEntry(ctx, sessionId, common.DATA_ACCOUNT_PIN, []byte(hashedPIN))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -1401,59 +1506,6 @@ func TestQuit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsValidPIN(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
pin string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "Valid PIN with 4 digits",
|
|
||||||
pin: "1234",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Valid PIN with leading zeros",
|
|
||||||
pin: "0001",
|
|
||||||
expected: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with less than 4 digits",
|
|
||||||
pin: "123",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with more than 4 digits",
|
|
||||||
pin: "12345",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with letters",
|
|
||||||
pin: "abcd",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Invalid PIN with special characters",
|
|
||||||
pin: "12@#",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Empty PIN",
|
|
||||||
pin: "",
|
|
||||||
expected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
actual := isValidPIN(tt.pin)
|
|
||||||
if actual != tt.expected {
|
|
||||||
t.Errorf("isValidPIN(%q) = %v; expected %v", tt.pin, actual, tt.expected)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateAmount(t *testing.T) {
|
func TestValidateAmount(t *testing.T) {
|
||||||
fm, err := NewFlagManager(flagsPath)
|
fm, err := NewFlagManager(flagsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1700,7 +1752,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1712,7 +1764,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
"Jina: %s\nJinsia: %s\nUmri: %s\nEneo: %s\nUnauza: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1724,7 +1776,7 @@ func TestGetProfile(t *testing.T) {
|
|||||||
result: resource.Result{
|
result: resource.Result{
|
||||||
Content: fmt.Sprintf(
|
Content: fmt.Sprintf(
|
||||||
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
"Name: %s\nGender: %s\nAge: %s\nLocation: %s\nYou provide: %s\n",
|
||||||
"John Doee", "Male", "48", "Kilifi", "Bananas",
|
"John Doee", "Male", "49", "Kilifi", "Bananas",
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
119
internal/http/at/parse.go
Normal file
119
internal/http/at/parse.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(parts[len(parts)-1]), 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 (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.grassecon.net/urdt/ussd/internal/handlers"
|
"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 {
|
type ATSessionHandler struct {
|
||||||
*SessionHandler
|
*httpserver.SessionHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
func NewATSessionHandler(h handlers.RequestHandler) *ATSessionHandler {
|
||||||
return &ATSessionHandler{
|
return &ATSessionHandler{
|
||||||
SessionHandler: ToSessionHandler(h),
|
SessionHandler: httpserver.ToSessionHandler(h),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,21 +34,21 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
|
|
||||||
rp := ash.GetRequestParser()
|
rp := ash.GetRequestParser()
|
||||||
cfg := ash.GetConfig()
|
cfg := ash.GetConfig()
|
||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
ash.writeError(w, 400, err)
|
ash.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rqs.Config = cfg
|
rqs.Config = cfg
|
||||||
rqs.Input, err = rp.GetInput(req)
|
rqs.Input, err = rp.GetInput(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
ash.writeError(w, 400, err)
|
ash.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rqs, err = ash.Process(rqs)
|
rqs, err = ash.Process(rqs)
|
||||||
switch err {
|
switch err {
|
||||||
case nil: // set code to 200 if no err
|
case nil: // set code to 200 if no err
|
||||||
code = 200
|
code = 200
|
||||||
@@ -53,7 +59,7 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +67,13 @@ func (ash *ATSessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
w.Header().Set("Content-Type", "text/plain")
|
w.Header().Set("Content-Type", "text/plain")
|
||||||
rqs, err = ash.Output(rqs)
|
rqs, err = ash.Output(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
rqs, err = ash.Reset(rqs)
|
rqs, err = ash.Reset(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ash.writeError(w, 500, err)
|
ash.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,4 +95,4 @@ func (ash *ATSessionHandler) Output(rqs handlers.RequestSession) (handlers.Reque
|
|||||||
|
|
||||||
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
_, err = rqs.Engine.Flush(rqs.Ctx, rqs.Writer)
|
||||||
return rqs, err
|
return rqs, err
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package http
|
package at
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@@ -16,16 +15,6 @@ import (
|
|||||||
"git.grassecon.net/urdt/ussd/internal/testutil/mocks/httpmocks"
|
"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) {
|
func TestNewATSessionHandler(t *testing.T) {
|
||||||
mockHandler := &httpmocks.MockRequestHandler{}
|
mockHandler := &httpmocks.MockRequestHandler{}
|
||||||
ash := NewATSessionHandler(mockHandler)
|
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
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -14,35 +13,6 @@ var (
|
|||||||
logg = logging.NewVanilla().WithDomain("httpserver")
|
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 {
|
type SessionHandler struct {
|
||||||
handlers.RequestHandler
|
handlers.RequestHandler
|
||||||
}
|
}
|
||||||
@@ -53,40 +23,39 @@ 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()
|
s := err.Error()
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
w.Header().Set("Content-Length", strconv.Itoa(len(s)))
|
||||||
w.WriteHeader(code)
|
w.WriteHeader(code)
|
||||||
_, err = w.Write([]byte{})
|
_, err = w.Write([]byte(s))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
logg.Errorf("error writing error!!", "err", err, "olderr", s)
|
||||||
w.WriteHeader(500)
|
w.WriteHeader(500)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
func (f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||||
var code int
|
var code int
|
||||||
var err error
|
var err error
|
||||||
var perr error
|
var perr error
|
||||||
|
|
||||||
rqs := handlers.RequestSession{
|
rqs := handlers.RequestSession{
|
||||||
Ctx: req.Context(),
|
Ctx: req.Context(),
|
||||||
Writer: w,
|
Writer: w,
|
||||||
}
|
}
|
||||||
|
|
||||||
rp := f.GetRequestParser()
|
rp := f.GetRequestParser()
|
||||||
cfg := f.GetConfig()
|
cfg := f.GetConfig()
|
||||||
cfg.SessionId, err = rp.GetSessionId(req)
|
cfg.SessionId, err = rp.GetSessionId(req.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
f.writeError(w, 400, err)
|
f.WriteError(w, 400, err)
|
||||||
}
|
}
|
||||||
rqs.Config = cfg
|
rqs.Config = cfg
|
||||||
rqs.Input, err = rp.GetInput(req)
|
rqs.Input, err = rp.GetInput(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
logg.ErrorCtxf(rqs.Ctx, "", "header processing error", err)
|
||||||
f.writeError(w, 400, err)
|
f.WriteError(w, 400, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +72,7 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if code != 200 {
|
if code != 200 {
|
||||||
f.writeError(w, 500, err)
|
f.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,11 +81,11 @@ func(f *SessionHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||||||
rqs, err = f.Output(rqs)
|
rqs, err = f.Output(rqs)
|
||||||
rqs, perr = f.Reset(rqs)
|
rqs, perr = f.Reset(rqs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
f.writeError(w, 500, err)
|
f.WriteError(w, 500, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
f.writeError(w, 500, perr)
|
f.WriteError(w, 500, perr)
|
||||||
return
|
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()
|
||||||
|
}
|
||||||
282
internal/ssh/ssh.go
Normal file
282
internal/ssh/ssh.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
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) {
|
||||||
|
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) {
|
||||||
|
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"
|
"git.defalsify.org/vise.git/db"
|
||||||
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
gdbmdb "git.defalsify.org/vise.git/db/gdbm"
|
||||||
"git.defalsify.org/vise.git/lang"
|
"git.defalsify.org/vise.git/lang"
|
||||||
|
"git.defalsify.org/vise.git/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logg = logging.NewVanilla().WithDomain("gdbmstorage")
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
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"
|
"git.defalsify.org/vise.git/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DATATYPE_EXTEND = 128
|
||||||
|
)
|
||||||
|
|
||||||
type Storage struct {
|
type Storage struct {
|
||||||
Persister *persist.Persister
|
Persister *persist.Persister
|
||||||
UserdataDb db.Db
|
UserdataDb db.Db
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"git.defalsify.org/vise.git/logging"
|
"git.defalsify.org/vise.git/logging"
|
||||||
"git.defalsify.org/vise.git/persist"
|
"git.defalsify.org/vise.git/persist"
|
||||||
"git.defalsify.org/vise.git/resource"
|
"git.defalsify.org/vise.git/resource"
|
||||||
"git.grassecon.net/urdt/ussd/initializers"
|
gdbmstorage "git.grassecon.net/urdt/ussd/internal/storage/db/gdbm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -23,63 +23,48 @@ type StorageService interface {
|
|||||||
GetPersister(ctx context.Context) (*persist.Persister, error)
|
GetPersister(ctx context.Context) (*persist.Persister, error)
|
||||||
GetUserdataDb(ctx context.Context) db.Db
|
GetUserdataDb(ctx context.Context) db.Db
|
||||||
GetResource(ctx context.Context) (resource.Resource, error)
|
GetResource(ctx context.Context) (resource.Resource, error)
|
||||||
EnsureDbDir() error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuStorageService struct {
|
type MenuStorageService struct {
|
||||||
dbDir string
|
conn ConnData
|
||||||
resourceDir string
|
resourceDir string
|
||||||
resourceStore db.Db
|
resourceStore db.Db
|
||||||
stateStore db.Db
|
stateStore db.Db
|
||||||
userDataStore db.Db
|
userDataStore db.Db
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildConnStr() string {
|
func NewMenuStorageService(conn ConnData, resourceDir string) *MenuStorageService {
|
||||||
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 {
|
|
||||||
return &MenuStorageService{
|
return &MenuStorageService{
|
||||||
dbDir: dbDir,
|
conn: conn,
|
||||||
resourceDir: resourceDir,
|
resourceDir: resourceDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, fileName string) (db.Db, error) {
|
func (ms *MenuStorageService) getOrCreateDb(ctx context.Context, existingDb db.Db, section string) (db.Db, error) {
|
||||||
database, ok := ctx.Value("Database").(string)
|
var newDb db.Db
|
||||||
if !ok {
|
var err error
|
||||||
return nil, fmt.Errorf("failed to select the database")
|
|
||||||
}
|
|
||||||
|
|
||||||
if existingDb != nil {
|
if existingDb != nil {
|
||||||
return 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()
|
newDb = postgres.NewPgDb()
|
||||||
connStr := buildConnStr()
|
} else if dbTyp == DBTYPE_GDBM {
|
||||||
err = newDb.Connect(ctx, connStr)
|
err = ms.ensureDbDir()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
connStr = path.Join(connStr, section)
|
||||||
|
newDb = gdbmstorage.NewThreadGdbmDb()
|
||||||
} else {
|
} else {
|
||||||
newDb = NewThreadGdbmDb()
|
return nil, fmt.Errorf("unsupported connection string: '%s'\n", ms.conn.String())
|
||||||
storeFile := path.Join(ms.dbDir, fileName)
|
|
||||||
err = newDb.Connect(ctx, storeFile)
|
|
||||||
}
|
}
|
||||||
|
logg.DebugCtxf(ctx, "connecting to db", "conn", connStr)
|
||||||
|
err = newDb.Connect(ctx, connStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -136,8 +121,8 @@ func (ms *MenuStorageService) GetStateStore(ctx context.Context) (db.Db, error)
|
|||||||
return ms.stateStore, nil
|
return ms.stateStore, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ms *MenuStorageService) EnsureDbDir() error {
|
func (ms *MenuStorageService) ensureDbDir() error {
|
||||||
err := os.MkdirAll(ms.dbDir, 0700)
|
err := os.MkdirAll(ms.conn.String(), 0700)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("state dir create exited with error: %v\n", err)
|
return fmt.Errorf("state dir create exited with error: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.defalsify.org/vise.git/engine"
|
"git.defalsify.org/vise.git/engine"
|
||||||
@@ -27,7 +28,6 @@ var (
|
|||||||
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
ctx = context.WithValue(ctx, "SessionId", sessionId)
|
||||||
ctx = context.WithValue(ctx, "Database", "gdbm")
|
|
||||||
pfp := path.Join(scriptDir, "pp.csv")
|
pfp := path.Join(scriptDir, "pp.csv")
|
||||||
|
|
||||||
var eventChannel = make(chan bool)
|
var eventChannel = make(chan bool)
|
||||||
@@ -39,37 +39,40 @@ func TestEngine(sessionId string) (engine.Engine, func(), chan bool) {
|
|||||||
FlagCount: uint32(128),
|
FlagCount: uint32(128),
|
||||||
}
|
}
|
||||||
|
|
||||||
dbDir := ".test_state"
|
connStr, err := filepath.Abs(".test_state/state.gdbm")
|
||||||
resourceDir := scriptDir
|
|
||||||
menuStorageService := storage.NewMenuStorageService(dbDir, resourceDir)
|
|
||||||
|
|
||||||
err := menuStorageService.EnsureDbDir()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "connstr err: %v", err)
|
||||||
os.Exit(1)
|
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)
|
rs, err := menuStorageService.GetResource(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "resource error: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pe, err := menuStorageService.GetPersister(ctx)
|
pe, err := menuStorageService.GetPersister(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "persister error: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
userDataStore, err := menuStorageService.GetUserdataDb(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "userdb error: %v", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbResource, ok := rs.(*resource.DbResource)
|
dbResource, ok := rs.(*resource.DbResource)
|
||||||
if !ok {
|
if !ok {
|
||||||
fmt.Fprintf(os.Stderr, err.Error())
|
fmt.Fprintf(os.Stderr, "dbresource cast error")
|
||||||
os.Exit(1)
|
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
|
package httpmocks
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
// MockRequestParser implements the handlers.RequestParser interface for testing
|
// MockRequestParser implements the handlers.RequestParser interface for testing
|
||||||
type MockRequestParser struct {
|
type MockRequestParser struct {
|
||||||
GetSessionIdFunc func(any) (string, error)
|
GetSessionIdFunc func(any) (string, error)
|
||||||
GetInputFunc func(any) ([]byte, 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)
|
return m.GetSessionIdFunc(rq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
var isoCodes = map[string]bool{
|
var isoCodes = map[string]bool{
|
||||||
"eng": true, // English
|
"eng": true, // English
|
||||||
"swa": true, // Swahili
|
"swa": true, // Swahili
|
||||||
|
"default": true, // Default language: English
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsValidISO639(code string) bool {
|
func IsValidISO639(code string) bool {
|
||||||
|
|||||||
@@ -62,10 +62,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1234",
|
"input": "1234",
|
||||||
"expectedContent": "Select language:\n0:English\n1:Kiswahili"
|
"expectedContent": "Select language:\n1:English\n2:Kiswahili"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
"expectedContent": "Your language change request was successful.\n0:Back\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1234",
|
"input": "1234",
|
||||||
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 79\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back"
|
"expectedContent": "My profile:\nName: foo bar\nGender: male\nAge: 80\nLocation: Kilifi\nYou provide: Bananas\n\n0:Back\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "0",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -17,7 +18,6 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
testData = driver.ReadData()
|
testData = driver.ReadData()
|
||||||
testStore = ".test_state"
|
|
||||||
sessionID string
|
sessionID string
|
||||||
src = rand.NewSource(42)
|
src = rand.NewSource(42)
|
||||||
g = rand.New(src)
|
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")
|
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 {
|
func GenerateSessionId() string {
|
||||||
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
uu := uuid.NewGenWithOptions(uuid.WithRandomReader(g))
|
||||||
v, err := uu.NewV4()
|
v, err := uu.NewV4()
|
||||||
@@ -81,8 +86,8 @@ func extractSendAmount(response []byte) string {
|
|||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
sessionID = GenerateSessionId()
|
sessionID = GenerateSessionId()
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := os.RemoveAll(testStore); err != nil {
|
if err := os.RemoveAll(testStore()); err != nil {
|
||||||
log.Fatalf("Failed to delete state store %s: %v", testStore, err)
|
log.Fatalf("Failed to delete state store %s: %v", testStore(), err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
m.Run()
|
m.Run()
|
||||||
@@ -298,9 +303,10 @@ func TestMainMenuSend(t *testing.T) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
sessions := testData
|
sessions := testData
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
groups := driver.FilterGroupsByName(session.Groups, "send_with_invalid_inputs")
|
groups := driver.FilterGroupsByName(session.Groups, "send_with_invite")
|
||||||
for _, group := range groups {
|
for _, group := range groups {
|
||||||
for _, step := range group.Steps {
|
for index, step := range group.Steps {
|
||||||
|
t.Logf("step %v with input %v", index, step.Input)
|
||||||
cont, err := en.Exec(ctx, []byte(step.Input))
|
cont, err := en.Exec(ctx, []byte(step.Input))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
t.Fatalf("Test case '%s' failed at input '%s': %v", group.Name, step.Input, err)
|
||||||
|
|||||||
@@ -7,14 +7,14 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"input": "",
|
"input": "",
|
||||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No"
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "0",
|
"input": "1",
|
||||||
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
"expectedContent": "Please enter a new four number PIN for your account:\n0:Exit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"input": "",
|
"input": "",
|
||||||
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n0:English\n1:Kiswahili"
|
"expectedContent": "Welcome to Sarafu Network\nPlease select a language\n1:English\n2:Kiswahili"
|
||||||
},
|
|
||||||
{
|
|
||||||
"input": "0",
|
|
||||||
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n0:Yes\n1:No"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1",
|
"input": "1",
|
||||||
|
"expectedContent": "Do you agree to terms and conditions?\nhttps://grassecon.org/pages/terms-and-conditions\n\n1:Yes\n2:No"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"input": "2",
|
||||||
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
"expectedContent": "Thank you for using Sarafu. Goodbye!"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -64,8 +64,8 @@
|
|||||||
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
"expectedContent": "Enter recipient's phone number/address/alias:\n0:Back"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "000",
|
"input": "0@0",
|
||||||
"expectedContent": "000 is invalid, please try again:\n1:Retry\n9:Quit"
|
"expectedContent": "0@0 is invalid, please try again:\n1:Retry\n9:Quit"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"input": "1",
|
"input": "1",
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ HALT
|
|||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP my_balance 1
|
INCMP my_balance 1
|
||||||
INCMP community_balance 2
|
INCMP community_balance 2
|
||||||
|
INCMP . *
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ LOAD reset_account_authorized 0
|
|||||||
LOAD reset_incorrect 0
|
LOAD reset_incorrect 0
|
||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT english 0
|
MOUT english 1
|
||||||
MOUT kiswahili 1
|
MOUT kiswahili 2
|
||||||
HALT
|
HALT
|
||||||
INCMP set_default 0
|
INCMP set_eng 1
|
||||||
INCMP set_swa 1
|
INCMP set_swa 2
|
||||||
INCMP . *
|
INCMP . *
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ MOUT quit 9
|
|||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP quit 9
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
|||||||
@@ -20,3 +20,4 @@ INCMP edit_yob 4
|
|||||||
INCMP edit_location 5
|
INCMP edit_location 5
|
||||||
INCMP edit_offerings 6
|
INCMP edit_offerings 6
|
||||||
INCMP view_profile 7
|
INCMP view_profile 7
|
||||||
|
INCMP . *
|
||||||
|
|||||||
@@ -14,3 +14,4 @@ INCMP balances 3
|
|||||||
INCMP check_statement 4
|
INCMP check_statement 4
|
||||||
INCMP pin_management 5
|
INCMP pin_management 5
|
||||||
INCMP address 6
|
INCMP address 6
|
||||||
|
INCMP . *
|
||||||
|
|||||||
@@ -9,3 +9,4 @@ MOUT quit 9
|
|||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
INCMP quit 9
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MOUT english 0
|
MOUT english 1
|
||||||
MOUT kiswahili 1
|
MOUT kiswahili 2
|
||||||
HALT
|
HALT
|
||||||
INCMP set_eng 0
|
INCMP set_eng 1
|
||||||
INCMP set_swa 1
|
INCMP set_swa 2
|
||||||
INCMP . *
|
INCMP . *
|
||||||
|
|||||||
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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
MOUT yes 0
|
MOUT yes 1
|
||||||
MOUT no 1
|
MOUT no 2
|
||||||
HALT
|
HALT
|
||||||
INCMP create_pin 0
|
INCMP create_pin 1
|
||||||
INCMP quit *
|
INCMP quit *
|
||||||
|
|||||||
@@ -4,5 +4,8 @@ LOAD reset_incorrect 6
|
|||||||
CATCH incorrect_pin flag_incorrect_pin 1
|
CATCH incorrect_pin flag_incorrect_pin 1
|
||||||
CATCH pin_entry flag_account_authorized 0
|
CATCH pin_entry flag_account_authorized 0
|
||||||
MOUT back 0
|
MOUT back 0
|
||||||
|
MOUT quit 9
|
||||||
HALT
|
HALT
|
||||||
INCMP _ 0
|
INCMP _ 0
|
||||||
|
INCMP quit 9
|
||||||
|
INCMP . *
|
||||||
|
|||||||
Reference in New Issue
Block a user