Support macaroons and TLS && Add arkd wallet cmds (#232)

* Update protos

* Update handlers

* Support macaroons and TLS

* Add arkd cli

* Minor fixes

* Update deps

* Fixes

* Update makefile

* Fixes

* Fix

* Fix

* Fix

* Remove trusted onboarding from client

* Completely remove trusted onboarding

* Fix compose files and add --no-macaroon flag to arkd cli

* Lint

* Remove e2e for trusted onboarding

* Add sleep time
This commit is contained in:
Pietralberto Mazza
2024-08-09 17:59:31 +02:00
committed by GitHub
parent 059e837794
commit 57ce08f239
105 changed files with 12111 additions and 1617 deletions

447
server/cmd/arkd/commands.go Normal file
View File

@@ -0,0 +1,447 @@
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/urfave/cli/v2"
"gopkg.in/macaroon.v2"
)
// flags
var (
passwordFlag = &cli.StringFlag{
Name: "password",
Usage: "wallet password",
Required: true,
}
mnemonicFlag = &cli.StringFlag{
Name: "mnemonic",
Usage: "mnemonic from which restore the wallet",
}
gapLimitFlag = &cli.Uint64Flag{
Name: "addr-gap-limit",
Usage: "address gap limit for wallet restoration",
Value: 100,
}
)
// commands
var (
walletCmd = &cli.Command{
Name: "wallet",
Usage: "Manage the Ark Server wallet",
Subcommands: append(
cli.Commands{},
walletStatusCmd,
walletCreateOrRestoreCmd,
walletUnlockCmd,
walletAddressCmd,
walletBalanceCmd,
),
}
walletStatusCmd = &cli.Command{
Name: "status",
Usage: "Get info about the status of the wallet",
Action: walletStatusAction,
}
walletCreateOrRestoreCmd = &cli.Command{
Name: "create",
Usage: "Create or restore the wallet",
Action: walletCreateOrRestoreAction,
Flags: []cli.Flag{passwordFlag, mnemonicFlag, gapLimitFlag},
}
walletUnlockCmd = &cli.Command{
Name: "unlock",
Usage: "Unlock the wallet",
Action: walletUnlockAction,
Flags: []cli.Flag{passwordFlag},
}
walletAddressCmd = &cli.Command{
Name: "address",
Usage: "Generate a receiving address",
Action: walletAddressAction,
}
walletBalanceCmd = &cli.Command{
Name: "balance",
Usage: "Get the wallet balance",
Action: walletBalanceAction,
}
)
func walletStatusAction(ctx *cli.Context) error {
baseURL := ctx.String("url")
tlsCertPath := ctx.String("tls-cert-path")
if strings.Contains(baseURL, "http://") {
tlsCertPath = ""
}
url := fmt.Sprintf("%s/v1/admin/wallet/status", baseURL)
status, err := getStatus(url, tlsCertPath)
if err != nil {
return err
}
fmt.Println(status)
return nil
}
func walletCreateOrRestoreAction(ctx *cli.Context) error {
baseURL := ctx.String("url")
password := ctx.String("password")
mnemonic := ctx.String("mnemonic")
gapLimit := ctx.Uint64("addr-gap-limit")
tlsCertPath := ctx.String("tls-cert-path")
if strings.Contains(baseURL, "http://") {
tlsCertPath = ""
}
if len(mnemonic) > 0 {
url := fmt.Sprintf("%s/v1/admin/wallet/restore", baseURL)
body := fmt.Sprintf(
`{"seed": "%s", "password": "%s", "gap_limit": %d}`,
mnemonic, password, gapLimit,
)
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
return err
}
fmt.Println("wallet restored")
return nil
}
url := fmt.Sprintf("%s/v1/admin/wallet/seed", baseURL)
seed, err := get[string](url, "seed", "", tlsCertPath)
if err != nil {
return err
}
url = fmt.Sprintf("%s/v1/admin/wallet/create", baseURL)
body := fmt.Sprintf(
`{"seed": "%s", "password": "%s"}`, seed, password,
)
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
return err
}
fmt.Println(seed)
return nil
}
func walletUnlockAction(ctx *cli.Context) error {
baseURL := ctx.String("url")
password := ctx.String("password")
url := fmt.Sprintf("%s/v1/admin/wallet/unlock", baseURL)
body := fmt.Sprintf(`{"password": "%s"}`, password)
tlsCertPath := ctx.String("tls-cert-path")
if strings.Contains(baseURL, "http://") {
tlsCertPath = ""
}
if _, err := post[struct{}](url, body, "", "", tlsCertPath); err != nil {
return err
}
fmt.Println("wallet unlocked")
return nil
}
func walletAddressAction(ctx *cli.Context) error {
baseURL := ctx.String("url")
var macaroon string
if !ctx.Bool("no-macaroon") {
macaroonPath := ctx.String("macaroon-path")
mac, err := getMacaroon(macaroonPath)
if err != nil {
return err
}
macaroon = mac
}
tlsCertPath := ctx.String("tls-cert-path")
if strings.Contains(baseURL, "http://") {
tlsCertPath = ""
}
url := fmt.Sprintf("%s/v1/admin/wallet/address", baseURL)
addr, err := get[string](url, "address", macaroon, tlsCertPath)
if err != nil {
return err
}
fmt.Println(addr)
return nil
}
func walletBalanceAction(ctx *cli.Context) error {
baseURL := ctx.String("url")
var macaroon string
if !ctx.Bool("no-macaroon") {
macaroonPath := ctx.String("macaroon-path")
mac, err := getMacaroon(macaroonPath)
if err != nil {
return err
}
macaroon = mac
}
tlsCertPath := ctx.String("tls-cert-path")
if strings.Contains(baseURL, "http://") {
tlsCertPath = ""
}
url := fmt.Sprintf("%s/v1/admin/wallet/balance", baseURL)
balance, err := getBalance(url, macaroon, tlsCertPath)
if err != nil {
return err
}
fmt.Println(balance)
return nil
}
func post[T any](url, body, key, macaroon, tlsCert string) (result T, err error) {
tlsConfig, err := getTLSConfig(tlsCert)
if err != nil {
return
}
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil {
return
}
req.Header.Add("Content-Type", "application/json")
if len(macaroon) > 0 {
req.Header.Add("X-Macaroon", macaroon)
}
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(string(buf))
return
}
if key == "" {
return
}
res := make(map[string]T)
if err = json.Unmarshal(buf, &res); err != nil {
return
}
result = res[key]
return
}
func get[T any](url, key, macaroon, tlsCert string) (result T, err error) {
tlsConfig, err := getTLSConfig(tlsCert)
if err != nil {
return
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return
}
req.Header.Add("Content-Type", "application/json")
if len(macaroon) > 0 {
req.Header.Add("X-Macaroon", macaroon)
}
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(string(buf))
return
}
res := make(map[string]T)
if err = json.Unmarshal(buf, &res); err != nil {
return
}
result = res[key]
return
}
type accountBalance struct {
Available string `json:"available"`
Locked string `json:"locked"`
}
func (b accountBalance) String() string {
return fmt.Sprintf(" available: %s\n locked: %s", b.Available, b.Locked)
}
type balance struct {
MainAccount accountBalance `json:"mainAccount"`
ConnectorsAccount accountBalance `json:"connectorsAccount"`
}
func (b balance) String() string {
return fmt.Sprintf(
"main account\n%s\nconnectors account\n%s",
b.MainAccount, b.ConnectorsAccount,
)
}
func getBalance(url, macaroon, tlsCert string) (*balance, error) {
tlsConfig, err := getTLSConfig(tlsCert)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
if len(macaroon) > 0 {
req.Header.Add("X-Macaroon", macaroon)
}
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(string(buf))
return nil, err
}
result := &balance{}
if err := json.Unmarshal(buf, result); err != nil {
return nil, err
}
return result, nil
}
type status struct {
Initialized bool `json:"initialized"`
Unlocked bool `json:"unlocked"`
Synced bool `json:"synced"`
}
func (s status) String() string {
return fmt.Sprintf(
"initialized: %t\nunlocked: %t\nsynced: %t",
s.Initialized, s.Unlocked, s.Synced,
)
}
func getStatus(url, tlsCert string) (*status, error) {
tlsConfig, err := getTLSConfig(tlsCert)
if err != nil {
return nil, err
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Content-Type", "application/json")
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
err = fmt.Errorf(string(buf))
return nil, err
}
result := &status{}
if err := json.Unmarshal(buf, result); err != nil {
return nil, err
}
return result, nil
}
func getMacaroon(path string) (string, error) {
macBytes, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("failed to read macaroon %s: %s", path, err)
}
mac := &macaroon.Macaroon{}
if err := mac.UnmarshalBinary(macBytes); err != nil {
return "", fmt.Errorf("failed to parse macaroon %s: %s", path, err)
}
return hex.EncodeToString(macBytes), nil
}
func getTLSConfig(path string) (*tls.Config, error) {
if len(path) <= 0 {
return nil, nil
}
var buf []byte
buf, err := os.ReadFile(path)
if err != nil {
return nil, err
}
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(buf); !ok {
return nil, fmt.Errorf("failed to parse tls cert")
}
return &tls.Config{
MinVersion: tls.VersionTLS12,
RootCAs: caCertPool,
}, nil
}

View File

@@ -1,14 +1,18 @@
package main
import (
"fmt"
"os"
"os/signal"
"path/filepath"
"syscall"
"github.com/ark-network/ark/common"
appconfig "github.com/ark-network/ark/internal/app-config"
"github.com/ark-network/ark/internal/config"
grpcservice "github.com/ark-network/ark/internal/interface/grpc"
log "github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
)
//nolint:all
@@ -18,19 +22,45 @@ var (
date = "unknown"
)
func main() {
// flags
var (
urlFlag = &cli.StringFlag{
Name: "url",
Usage: "the url where to reach ark server",
Value: fmt.Sprintf("http://localhost:%d", config.DefaultPort),
}
noMacaroonFlag = &cli.BoolFlag{
Name: "no-macaroon",
Usage: "don't use macaroon auth",
Value: false,
}
macaroonFlag = &cli.StringFlag{
Name: "macaroon-path",
Usage: "the path where to find the admin macaroon file",
Value: filepath.Join(common.AppDataDir("arkd", false), "macaroons", "admin.macaroon"),
}
tlsCertFlag = &cli.StringFlag{
Name: "tls-cert-path",
Usage: "the path where to find the TLS certificate",
Value: filepath.Join(common.AppDataDir("arkd", false), "tls", "cert.pem"),
}
)
func mainAction(_ *cli.Context) error {
cfg, err := config.LoadConfig()
if err != nil {
log.WithError(err).Fatal("invalid config")
return fmt.Errorf("invalid config: %s", err)
}
log.SetLevel(log.Level(cfg.LogLevel))
svcConfig := grpcservice.Config{
Port: cfg.Port,
NoTLS: cfg.NoTLS,
AuthUser: cfg.AuthUser,
AuthPass: cfg.AuthPass,
Datadir: cfg.Datadir,
Port: cfg.Port,
NoTLS: cfg.NoTLS,
NoMacaroons: cfg.NoMacaroons,
TLSExtraIPs: cfg.TLSExtraIPs,
TLSExtraDomains: cfg.TLSExtraDomains,
}
appConfig := &appconfig.Config{
@@ -53,14 +83,14 @@ func main() {
}
svc, err := grpcservice.NewService(svcConfig, appConfig)
if err != nil {
log.Fatal(err)
return err
}
log.RegisterExitHandler(svc.Stop)
log.Info("starting service...")
if err := svc.Start(); err != nil {
log.Fatal(err)
return err
}
sigChan := make(chan os.Signal, 1)
@@ -69,4 +99,20 @@ func main() {
log.Info("shutting down service...")
log.Exit(0)
return nil
}
func main() {
app := cli.NewApp()
app.Version = version
app.Name = "Arkd CLI"
app.Usage = "arkd command line interface"
app.Commands = append(app.Commands, walletCmd)
app.Action = mainAction
app.Flags = append(app.Flags, urlFlag, noMacaroonFlag, macaroonFlag, tlsCertFlag)
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}