cli.go (15586B)
1 package main 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "crypto/rand" 8 "encoding/base64" 9 "errors" 10 "fmt" 11 "os" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/jackc/pgx/v5" 17 "github.com/jackc/pgx/v5/pgxpool" 18 "golang.org/x/crypto/argon2" 19 "gopkg.in/ini.v1" 20 ) 21 22 const ACTION_HELP = "h" 23 const ACTION_SETUP_SIMULATION = "sim" 24 const ACTION_REGISTER_PROVIDER = "rp" 25 const ACTION_REGISTER_TERMINAL = "rt" 26 const ACTION_DEACTIVATE_TERMINAL = "dt" 27 const ACTION_ACTIVATE_TERMINAL = "at" 28 const ACTION_WITHDRAWAL_INFOMRATION = "w" 29 const ACTION_WITHDRAWAL_INFOMRATION_BY_PTID = "wp" 30 const ACTION_PROVIDER_CREDENTIALS = "wc" 31 const ACTION_CONNECT_DB = "db" 32 const ACTION_QUIT = "q" 33 34 // format of wallee backend credentials. 35 type WalleeCredentials struct { 36 SpaceId int `json:"spaceId"` 37 UserId int `json:"userId"` 38 ApplicationUserKey string `json:"application-user-key"` 39 } 40 41 var DB *pgxpool.Pool 42 43 // enter database credentials (host, port, database, username, password) 44 // register terminal -> read password, hash, save to database 45 // register provider -> read wallee, read space(id), read userid, read password, hash what needs to be hashed, save to database 46 47 func main() { 48 49 iniFilePath := "" 50 connstr := "" 51 if len(os.Args) > 1 { 52 53 nextIsConf := false 54 for i, arg := range os.Args { 55 if i == 0 { 56 continue 57 } else if nextIsConf { 58 iniFilePath = arg 59 nextIsConf = false 60 } else if arg == "-c" { 61 nextIsConf = true 62 } else if arg == "-h" { 63 showHelp() 64 os.Exit(0) 65 } 66 } 67 } 68 69 if iniFilePath != "" { 70 optionalPgConnStr, err := parseDbConnstrFromIni(iniFilePath) 71 if err != nil { 72 fmt.Println("failed parsing config:", err.Error()) 73 } 74 fmt.Println("read connection string from ini:", optionalPgConnStr) 75 connstr = optionalPgConnStr 76 } 77 78 if connstr != "" { 79 err := connectDbUsingString(connstr) 80 if err != nil { 81 fmt.Println("error while connecting to database, using connection string from config. error:", err.Error()) 82 } 83 } 84 fmt.Println("What do you want to do?") 85 showHelp() 86 for { 87 err := dispatchCommand(read("Type command (term in brackets): ")) 88 if err != nil { 89 fmt.Println("Error occured:", err.Error()) 90 } 91 } 92 } 93 94 func parseDbConnstrFromIni(path string) (string, error) { 95 96 f, err := os.Open(path) 97 if err != nil { 98 return "", err 99 } 100 defer f.Close() 101 102 stat, err := f.Stat() 103 if err != nil { 104 return "", err 105 } 106 107 content := make([]byte, stat.Size()) 108 _, err = f.Read(content) 109 if err != nil { 110 return "", err 111 } 112 113 ini, err := ini.Load(content) 114 if err != nil { 115 return "", err 116 } 117 118 section := ini.Section("database") 119 if section == nil { 120 fmt.Println("database section not configured in ini file") 121 os.Exit(0) 122 } 123 124 value, err := section.GetKey("CONFIG") 125 if err != nil { 126 return "", err 127 } 128 129 return value.String(), nil 130 } 131 132 func walleePrepareCredentials() (string, string, string, string, error) { 133 134 name := "Wallee" 135 paytotargettype := "wallee-transaction" 136 backendUrl := read("Wallee backend base url: ") 137 spaceIdStr := read("Wallee Space Id: ") 138 spaceId, err := strconv.Atoi(spaceIdStr) 139 if err != nil { 140 return "", "", "", "", err 141 } 142 userIdStr := read("Wallee User Id: ") 143 userId, err := strconv.Atoi(userIdStr) 144 if err != nil { 145 return "", "", "", "", err 146 } 147 key := read("Wallee Application User Key: ") 148 hashedKey, err := pbkdf(key) 149 if err != nil { 150 return "", "", "", "", err 151 } 152 153 creds, err := NewJsonCodec[WalleeCredentials]().EncodeToBytes(&WalleeCredentials{ 154 SpaceId: spaceId, 155 UserId: userId, 156 ApplicationUserKey: hashedKey, 157 }) 158 if err != nil { 159 return "", "", "", "", err 160 } 161 credsEncoded := base64.StdEncoding.EncodeToString(creds) 162 163 return name, paytotargettype, backendUrl, credsEncoded, nil 164 } 165 166 func generateProviderCredentials() error { 167 168 name, paytottargettype, backendUrl, credsEncoded, err := walleePrepareCredentials() 169 if err != nil { 170 return err 171 } 172 173 fmt.Println("provider-name:", name) 174 fmt.Println("provider-payto-target-type:", paytottargettype) 175 fmt.Println("provider-backend-url:", backendUrl) 176 fmt.Println("base64 encoded credentials (can be added to the database like this):", credsEncoded) 177 178 return nil 179 } 180 181 func registerWalleeProvider() error { 182 183 if DB == nil { 184 return errors.New("connect to the database first (cmd: db)") 185 } 186 187 name, paytotargettype, backendUrl, credsEncoded, err := walleePrepareCredentials() 188 if err != nil { 189 return err 190 } 191 192 _, err = DB.Exec( 193 context.Background(), 194 INSERT_PROVIDER, 195 name, 196 paytotargettype, 197 backendUrl, 198 credsEncoded, 199 ) 200 if err != nil { 201 return err 202 } 203 204 return nil 205 } 206 207 func registerWalleeTerminal() error { 208 209 if DB == nil { 210 return errors.New("connect to the database first (cmd: db)") 211 } 212 213 description := read("Description (location, inventory identifier, etc.): ") 214 providerName := read("Provider Name: ") 215 216 rows, err := DB.Query( 217 context.Background(), 218 GET_PROVIDER_BY_NAME, 219 providerName, 220 ) 221 if err != nil { 222 return err 223 } 224 225 p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider]) 226 if err != nil { 227 return err 228 } 229 fmt.Println("Collected provider by name") 230 rows.Close() // release rows / connection 231 232 accessToken := make([]byte, 32) 233 _, err = rand.Read(accessToken) 234 if err != nil { 235 return err 236 } 237 238 accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken) 239 240 hashedAccessToken, err := pbkdf(accessTokenBase64) 241 if err != nil { 242 return err 243 } 244 245 fmt.Println("adding terminal") 246 _, err = DB.Exec( 247 context.Background(), 248 INSERT_TERMINAL, 249 hashedAccessToken, 250 description, 251 p.ProviderId, 252 ) 253 if err != nil { 254 return err 255 } 256 257 fmt.Println("looking up last inserted terminal") 258 rows, err = DB.Query( 259 context.Background(), 260 GET_LAST_INSERTED_TERMINAL, 261 ) 262 if err != nil { 263 return err 264 } 265 t, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Terminal]) 266 if err != nil { 267 return err 268 } 269 rows.Close() 270 271 fmt.Println("Terminal-User-Id (used to identify terminal at the api. You want to note this):", "Wallee-"+strconv.Itoa(int(t.TerminalID))) 272 fmt.Println("GENERATED ACCESS-TOKEN (save it in your password manager. Can't be recovered!!):") 273 fmt.Println(accessTokenBase64) 274 275 return nil 276 } 277 278 func deactivateTerminal() error { 279 280 if DB == nil { 281 return errors.New("connect to the database first (cmd: db)") 282 } 283 284 fmt.Println("You are about to deactivate terminal which allows withdrawals. This will make the terminal unusable.") 285 tuid := read("Terminal-User-Id: ") 286 parts := strings.Split(tuid, "-") 287 if len(parts) != 2 { 288 return errors.New("invalid terminal-user-id (format=[PROVIDER_NAME]-[NUMBER])") 289 } 290 tid, err := strconv.Atoi(parts[1]) 291 if err != nil { 292 return err 293 } 294 295 _, err = DB.Exec( 296 context.Background(), 297 DEACTIVATE_TERMINAL, 298 tid, 299 ) 300 if err != nil { 301 return err 302 } 303 304 return nil 305 } 306 307 func activateTerminal() error { 308 309 if DB == nil { 310 return errors.New("connect to the database first (cmd: db)") 311 } 312 313 fmt.Println("You are about to activate a terminal which allows withdrawals. This will make the terminal operational.") 314 tuid := read("Terminal-User-Id: ") 315 parts := strings.Split(tuid, "-") 316 if len(parts) != 2 { 317 return errors.New("invalid terminal-user-id (format=[PROVIDER_NAME]-[NUMBER])") 318 } 319 tid, err := strconv.Atoi(parts[1]) 320 if err != nil { 321 return err 322 } 323 324 _, err = DB.Exec( 325 context.Background(), 326 ACTIVATE_TERMINAL, 327 tid, 328 ) 329 if err != nil { 330 return err 331 } 332 333 return nil 334 } 335 336 func withdrawalInformationByWopid() error { 337 338 if DB == nil { 339 return errors.New("connect to the database first (cmd: db)") 340 } 341 342 wopid := read("WOPID (encoded): ") 343 wopidDecoded, err := decodeCrock(wopid) 344 if err != nil { 345 return err 346 } 347 rows, err := DB.Query( 348 context.Background(), 349 GET_WITHDRAWAL_BY_WOPID, 350 wopidDecoded, 351 ) 352 if err != nil { 353 return err 354 } 355 356 return readPrintWithdrawal(rows) 357 } 358 359 func withdrawalInformationByProviderTransactionId() error { 360 361 if DB == nil { 362 return errors.New("connect to the database first (cmd: db)") 363 } 364 365 ptid := read("Provider Transaction ID: ") 366 rows, err := DB.Query( 367 context.Background(), 368 GET_WITHDRAWAL_BY_PROVIDER_TRANSACTION_ID, 369 ptid, 370 ) 371 if err != nil { 372 return err 373 } 374 375 return readPrintWithdrawal(rows) 376 } 377 378 func readPrintWithdrawal(rows pgx.Rows) error { 379 380 type TalerAmountCurrency struct { 381 Val int64 `db:"val"` 382 Frac int32 `db:"frac"` 383 Curr string `db:"curr"` 384 } 385 type Withdrawal struct { 386 WithdrawalRowId uint64 `db:"withdrawal_row_id"` 387 ConfirmedRowId uint64 `db:"confirmed_row_id"` 388 RequestUid string `db:"request_uid"` 389 Wopid []byte `db:"wopid"` 390 ReservePubKey []byte `db:"reserve_pub_key"` 391 RegistrationTs int64 `db:"registration_ts"` 392 Amount *TalerAmountCurrency `db:"amount" scan:"follow"` 393 SuggestedAmount *TalerAmountCurrency `db:"suggested_amount" scan:"follow"` 394 TerminalFees *TalerAmountCurrency `db:"terminal_fees" scan:"follow"` 395 WithdrawalStatus string `db:"withdrawal_status"` 396 TerminalId int `db:"terminal_id"` 397 ProviderTransactionId *string `db:"provider_transaction_id"` 398 LastRetryTs *int64 `db:"last_retry_ts"` 399 RetryCounter int32 `db:"retry_counter"` 400 CompletionProof []byte `db:"completion_proof"` 401 } 402 403 w, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Withdrawal]) 404 if err != nil { 405 return err 406 } 407 rows.Close() 408 409 indent := " -" 410 fmt.Println("Withdrawal:") 411 fmt.Println(indent, "wopid :", encodeCrock(w.Wopid)) 412 fmt.Println(indent, "status :", w.WithdrawalStatus) 413 fmt.Println(indent, "reserve public key:", encodeCrock(w.ReservePubKey)) 414 fmt.Println(indent, "provider tid :", *w.ProviderTransactionId) 415 fmt.Println(indent, "amount :", w.Amount) 416 fmt.Println(indent, "terminal :", w.TerminalId) 417 fmt.Println(indent, "attest retries :", w.RetryCounter) 418 if w.LastRetryTs != nil { 419 fmt.Println(indent, "last retry :", time.Unix(*w.LastRetryTs, 0).Format("yyyy-MM-dd hh:mm:ss")) 420 } 421 422 return nil 423 } 424 425 func setupSimulation() error { 426 427 if DB == nil { 428 return errors.New("connect to the database first (cmd: db)") 429 } 430 431 // SETTING UP PROVIDER 432 fmt.Println("Setting up simulation provider and terminal.") 433 name := "Simulation" 434 paytotargettype := "void" 435 backendUrl := "simulation provider will not contact any backend." 436 credsEncoded := base64.StdEncoding.EncodeToString(bytes.NewBufferString("simulation provider will not contact any backend.").Bytes()) 437 438 _, err := DB.Exec( 439 context.Background(), 440 INSERT_PROVIDER, 441 name, 442 paytotargettype, 443 backendUrl, 444 credsEncoded, 445 ) 446 if err != nil { 447 return err 448 } 449 450 // SETTING UP TERMINAL 451 description := "simulation terminal" 452 453 rows, err := DB.Query( 454 context.Background(), 455 GET_PROVIDER_BY_NAME, 456 name, 457 ) 458 if err != nil { 459 return err 460 } 461 462 p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider]) 463 if err != nil { 464 return err 465 } 466 rows.Close() // release rows / connection 467 468 accessToken := make([]byte, 32) 469 _, err = rand.Read(accessToken) 470 if err != nil { 471 return err 472 } 473 474 accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken) 475 476 hashedAccessToken, err := pbkdf(accessTokenBase64) 477 if err != nil { 478 return err 479 } 480 481 _, err = DB.Exec( 482 context.Background(), 483 INSERT_TERMINAL, 484 hashedAccessToken, 485 description, 486 p.ProviderId, 487 ) 488 if err != nil { 489 return err 490 } 491 492 fmt.Println("looking up last inserted terminal") 493 rows, err = DB.Query( 494 context.Background(), 495 GET_LAST_INSERTED_TERMINAL, 496 ) 497 if err != nil { 498 return err 499 } 500 t, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Terminal]) 501 if err != nil { 502 return err 503 } 504 rows.Close() 505 506 fmt.Println("Terminal-User-Id (used to identify terminal at the api. You want to note this):", name+"-"+strconv.Itoa(int(t.TerminalID))) 507 fmt.Println("GENERATED ACCESS-TOKEN (save it in your password manager. Can't be recovered!!):") 508 fmt.Println(accessTokenBase64) 509 510 return nil 511 } 512 513 func connectDatabase() error { 514 515 u := read("Username: ") 516 pw := read("Password: ") 517 h := read("Host: ") 518 ps := read("Port: ") 519 p, err := strconv.Atoi(ps) 520 if err != nil { 521 return err 522 } 523 d := read("Database: ") 524 525 connstring := PostgresConnectionString(u, pw, h, p, d) 526 return connectDbUsingString(connstring) 527 } 528 529 func connectDbUsingString(connString string) error { 530 dbCfg, err := pgxpool.ParseConfig(connString) 531 if err != nil { 532 return err 533 } 534 535 dbCfg.AfterConnect = registerCustomTypesHook 536 DB, err = pgxpool.NewWithConfig(context.Background(), dbCfg) 537 if err != nil { 538 return err 539 } 540 fmt.Println("connected to database") 541 return nil 542 } 543 544 func showHelp() error { 545 546 fmt.Println("register wallee provider (", ACTION_REGISTER_PROVIDER, ")") 547 fmt.Println("register wallee terminal (", ACTION_REGISTER_TERMINAL, ")") 548 fmt.Println("deactivate wallee terminal (", ACTION_DEACTIVATE_TERMINAL, ")") 549 fmt.Println("activate wallee terminal (", ACTION_ACTIVATE_TERMINAL, ")") 550 fmt.Println("setup simulation (", ACTION_SETUP_SIMULATION, ")") 551 fmt.Println("withdrawal information by wopid (", ACTION_WITHDRAWAL_INFOMRATION, ")") 552 fmt.Println("witdhrawal information by provider transaction id (", ACTION_WITHDRAWAL_INFOMRATION_BY_PTID, ")") 553 fmt.Println("create wallee provider credentials string (", ACTION_PROVIDER_CREDENTIALS, ")") 554 if DB == nil { 555 fmt.Println("connect database (", ACTION_CONNECT_DB, ")") 556 } 557 fmt.Println("show help (", ACTION_HELP, ")") 558 fmt.Println("quit (", ACTION_QUIT, ")") 559 return nil 560 } 561 562 func quit() error { 563 fmt.Println("bye...") 564 os.Exit(0) 565 return nil 566 } 567 568 func pbkdf(pw string) (string, error) { 569 570 rfcTime := 3 571 rfcMemory := 32 * 1024 572 salt := make([]byte, 16) 573 _, err := rand.Read(salt) 574 if err != nil { 575 return "", err 576 } 577 key := argon2.Key([]byte(pw), salt, uint32(rfcTime), uint32(rfcMemory), 4, 32) 578 579 keyAndSalt := make([]byte, 0, 48) 580 keyAndSalt = append(keyAndSalt, key...) 581 keyAndSalt = append(keyAndSalt, salt...) 582 if len(keyAndSalt) != 48 { 583 return "", errors.New("invalid password hash and salt") 584 } 585 return base64.StdEncoding.EncodeToString(keyAndSalt), nil 586 } 587 588 func PostgresConnectionString( 589 username string, 590 password string, 591 host string, 592 port int, 593 database string, 594 ) string { 595 return fmt.Sprintf( 596 "postgres://%s:%s@%s:%d/%s", 597 username, 598 password, 599 host, 600 port, 601 database, 602 ) 603 } 604 605 func dispatchCommand(cmd string) error { 606 607 var err error 608 switch cmd { 609 case ACTION_HELP: 610 err = showHelp() 611 case ACTION_QUIT: 612 err = quit() 613 case ACTION_CONNECT_DB: 614 err = connectDatabase() 615 case ACTION_REGISTER_PROVIDER: 616 err = registerWalleeProvider() 617 case ACTION_REGISTER_TERMINAL: 618 err = registerWalleeTerminal() 619 case ACTION_DEACTIVATE_TERMINAL: 620 err = deactivateTerminal() 621 case ACTION_ACTIVATE_TERMINAL: 622 err = activateTerminal() 623 case ACTION_WITHDRAWAL_INFOMRATION: 624 err = withdrawalInformationByWopid() 625 case ACTION_WITHDRAWAL_INFOMRATION_BY_PTID: 626 err = withdrawalInformationByProviderTransactionId() 627 case ACTION_PROVIDER_CREDENTIALS: 628 err = generateProviderCredentials() 629 case ACTION_SETUP_SIMULATION: 630 err = setupSimulation() 631 default: 632 fmt.Println("unknown action") 633 } 634 return err 635 } 636 637 func read(prefix string) string { 638 reader := bufio.NewReader(os.Stdin) 639 fmt.Print(prefix) 640 inp, err := reader.ReadString('\n') 641 if err != nil { 642 fmt.Println(err.Error()) 643 return "" 644 } 645 return strings.Trim(inp, "\n") 646 } 647 648 func registerCustomTypesHook(ctx context.Context, conn *pgx.Conn) error { 649 650 t, err := conn.LoadType(ctx, "c2ec.taler_amount_currency") 651 if err != nil { 652 return err 653 } 654 655 conn.TypeMap().RegisterType(t) 656 return nil 657 }