package client import ( "context" "crypto/ed25519" "crypto/rand" "encoding/base64" "errors" "flag" "fmt" "io" "log/slog" "net" "os" "numenor-labs.us/dsfx/dsfx/internal/lib/crypto/identity" "numenor-labs.us/dsfx/dsfx/internal/lib/disk" "numenor-labs.us/dsfx/dsfx/internal/lib/logging" "numenor-labs.us/dsfx/dsfx/internal/lib/network" "numenor-labs.us/dsfx/dsfx/internal/lib/storage/scoped" "numenor-labs.us/dsfx/dsfx/internal/lib/system" ) const ( // DefaultConfigDir is the default directory for the dsfxctl configuration. DefaultConfigDir = "/etc/dsfxctl" ) // Conf holds the configuration for the dsfxctl application. type Conf struct { // Directories ConfigDir string LogLevel string } // SlogLevel returns the appropriate slog.Level based on the LogLevel string. func (c Conf) SlogLevel() slog.Level { switch c.LogLevel { case "debug": return slog.LevelDebug case "info": return slog.LevelInfo case "warn": return slog.LevelWarn case "error": return slog.LevelError default: return slog.LevelInfo } } // Client represents the client application for dsfxctl. type Client struct { // resources disk disk.Disk system system.System // configuration 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 { flagConfig := flag.String("configDir", "/etc/dsfxctl", "Path to the configuration directory") flagLogLevel := flag.String("logLevel", "info", "The log level (debug, info, warn, error)") if !flag.Parsed() { flag.Parse() } conf := Conf{ LogLevel: *flagLogLevel, ConfigDir: *flagConfig, } 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: a.conf.SlogLevel(), } logger := slog.New(slog.NewTextHandler(a.system.Stdout(), opts)) err := a.disk.MkdirAll(a.conf.ConfigDir, 0755) if err != nil { logger.ErrorContext(ctx, "failed to create config directory", slog.Any("error", err)) return err } // 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) id, err := a.loadIdentity() if err != nil { logger.ErrorContext(ctx, "failed to load identity", slog.Any("error", err)) return err } 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) return nil case "identity": pubKey := identity.ToPublicKey(id) logger.InfoContext(ctx, "identity", slog.String("publicKey", base64.StdEncoding.EncodeToString(pubKey))) return nil case "": return errors.New("no command provided") default: return errors.New("unknown command: " + a.system.Arg(0)) } } 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() logger.InfoContext(ctx, "connected to server", slog.String("remote", raddr.String())) } // loadIdentity ... func (c *Client) loadIdentity() (ed25519.PrivateKey, error) { hasKeyFile, err := c.hasKeyFile() if err != nil { return nil, fmt.Errorf("failed to check for admins file: %w", err) } if !hasKeyFile { if err := c.createKeyFile(); err != nil { return nil, fmt.Errorf("failed to create admins file: %w", err) } } return c.readKeyFile() } // hasKeyFile ... func (c *Client) hasKeyFile() (bool, error) { f, err := c.configScope.Open("ed25519.key") if errors.Is(err, os.ErrNotExist) { return false, nil // Key file does not exist } if err != nil { return false, nil } defer f.Close() return true, nil } // createKeyFile ... func (c *Client) createKeyFile() error { f, err := c.configScope.Create("ed25519.key") if err != nil { return err } defer f.Close() _, privateKey, err := ed25519.GenerateKey(rand.Reader) if err != nil { return err } _, err = f.Write([]byte(base64.StdEncoding.EncodeToString(privateKey))) if err != nil { return err } return nil } // readKeyFile ... func (c *Client) readKeyFile() (ed25519.PrivateKey, error) { f, err := c.configScope.Open("ed25519.key") if err != nil { return nil, err } defer f.Close() keyRawBase64, err := io.ReadAll(f) if err != nil { return nil, err } keyRaw, err := base64.StdEncoding.DecodeString(string(keyRawBase64)) if err != nil { return nil, fmt.Errorf("failed to decode key: %w", err) } if len(keyRaw) != ed25519.PrivateKeySize { return nil, fmt.Errorf("key file is not the correct size: %d", len(keyRaw)) } return ed25519.PrivateKey(keyRaw), nil }