package client import ( "context" "crypto/ed25519" "encoding/base64" "errors" "log/slog" "net" "koti.casa/numenor-labs/dsfx/cmd/dsfxctl/conf" "koti.casa/numenor-labs/dsfx/pkg/crypto/identity" "koti.casa/numenor-labs/dsfx/pkg/disk" "koti.casa/numenor-labs/dsfx/pkg/logging" "koti.casa/numenor-labs/dsfx/pkg/network" "koti.casa/numenor-labs/dsfx/pkg/storage/scoped" "koti.casa/numenor-labs/dsfx/pkg/system" ) // Client represents the client application for dsfxctl. type Client struct { // resources disk disk.Disk system system.System // configuration conf conf.Conf // storage scopes configScope disk.Disk } // New creates a new Client instance with the provided disk, system, and // configuration. func New(disk disk.Disk, system system.System) *Client { conf := conf.FromSystem(system) return &Client{ // resources disk: disk, system: system, // configuration conf: conf, // storage scopes configScope: scoped.New(disk, conf.ConfigDir), } } // Run executes the main logic of the application. func (a *Client) Run(ctx context.Context) error { // --------------------------------------------------------------------------- // Logger opts := &slog.HandlerOptions{ AddSource: false, Level: slog.LevelDebug, } logger := slog.New(slog.NewTextHandler(a.system.Stdout(), opts)) // Everything in the application will attempt to use the logger in stored in // the context, but we also set the default with slog as a fallback. In cases // where the context is not available, or the context is not a child of the // context with the logger, the default logger will be used. slog.SetDefault(logger) ctx = logging.WithContext(ctx, logger) keyFile, err := a.configScope.Open("key") if err != nil { logger.WarnContext(ctx, "key file is missing, reinitializing") logger.WarnContext(ctx, "if this is your first time running dsfxctl, you can ignore this") } if keyFile == nil { logger.InfoContext(ctx, "generating new key") keyFile, err = a.configScope.Create("key") if err != nil { logger.ErrorContext(ctx, "failed to create key file", slog.Any("error", err)) return err } privkey, err := identity.Generate() if err != nil { logger.ErrorContext(ctx, "failed to generate key", slog.Any("error", err)) return err } _, err = keyFile.Write([]byte(base64.StdEncoding.EncodeToString(privkey))) if err != nil { logger.ErrorContext(ctx, "failed to write key", slog.Any("error", err)) return err } } defer keyFile.Close() keyRaw := make([]byte, ed25519.PrivateKeySize) n, err := keyFile.Read(keyRaw) if err != nil { logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err)) return err } if n != ed25519.PrivateKeySize { logger.ErrorContext(ctx, "key file is not the correct size", slog.Int("size", n)) return err } id := ed25519.PrivateKey(keyRaw) laddr := network.NewAddr( net.ParseIP("0.0.0.0"), 0, // port 0 means any available port identity.ToPublicKey(id), ) logger.DebugContext(ctx, "using addr", slog.String("address", laddr.String())) switch a.system.Arg(0) { case "test": raddrRaw := a.system.Arg(1) if raddrRaw == "" { logger.ErrorContext(ctx, "no remote address provided") return err } testConnection(ctx, id, laddr, raddrRaw) case "": return errors.New("no command provided") default: return errors.New("unknown command: " + a.system.Arg(0)) } return nil } func testConnection(ctx context.Context, id ed25519.PrivateKey, laddr *network.Addr, raddrRaw string) { logger := logging.FromContext(context.Background()) raddr, err := network.ParseAddr(raddrRaw) if err != nil { logger.ErrorContext(ctx, "failed to parse server address", slog.Any("error", err)) return } conn, err := network.Dial(ctx, id, laddr, raddr) if err != nil { logger.ErrorContext(ctx, "failed to connect", slog.Any("error", err)) return } defer conn.Close() }