From a74cdb8b02ed2a85e83ea48e09f992fb46d22e27 Mon Sep 17 00:00:00 2001 From: Dustin Stiles Date: Sat, 22 Mar 2025 13:06:51 -0400 Subject: [PATCH] fix(internal/peer): automatically create key and admin files --- cmd/dsfx/main.go | 19 +- docs/hosting.md | 18 ++ internal/lib/disk/default.go | 12 ++ internal/lib/disk/disk.go | 20 ++- internal/lib/storage/scoped/scoped.go | 5 + internal/peer/node/node.go | 241 ++++++++++++++------------ internal/sim/disk.go | 23 +++ 7 files changed, 203 insertions(+), 135 deletions(-) diff --git a/cmd/dsfx/main.go b/cmd/dsfx/main.go index 7715c82..9b7eb3b 100644 --- a/cmd/dsfx/main.go +++ b/cmd/dsfx/main.go @@ -2,30 +2,17 @@ package main import ( "context" - "log/slog" "koti.casa/numenor-labs/dsfx/internal/lib/disk" "koti.casa/numenor-labs/dsfx/internal/lib/system" "koti.casa/numenor-labs/dsfx/internal/peer/node" - - "koti.casa/numenor-labs/dsfx/internal/lib/storage/scoped" ) func main() { - ctx := context.Background() + n := node.New(disk.Default(), system.Default()) - sys := system.Default() - - configDir := sys.GetEnv("DSFXNODE_CONFIG_DIR") - if configDir == "" { - configDir = "/etc/dsfxnode/config" - } - configScope := scoped.New(disk.Default(), configDir) - - err := node.New(configScope, sys).Run(ctx) + err := n.Run(context.Background()) if err != nil { - // Log the error and exit with a non-zero status code. - slog.Error("Error running dsfxnode", slog.Any("error", err)) - sys.Exit(1) + panic(err) } } diff --git a/docs/hosting.md b/docs/hosting.md index cc7cee7..54fae98 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -52,3 +52,21 @@ DSFX uses the following environment variables to configure its behavior: | DSFX_PORT | The port on which the DSFX server will listen | 8000 | | DSFX_CONFIG_DIR | The directory where the DSFX configuration files are stored | /etc/dsfx/config | | DSFX_STORAGE_DIR | The directory where the DSFX storage files are stored | /etx/dsfx/data | + +## Local Files + +The DSFX server uses local files for configuration and storage. The default directories for these +files are specified in the `DSFX_CONFIG_DIR` and `DSFX_STORAGE_DIR` environment variables. You can +change these directories by setting the corresponding environment variables before starting the server. + +For docker installations, it is recommended to mount the local directories to the container +using the `-v` flag. For example: + +```bash +docker run -d \ + --name dsfx \ + -p 8000:8000 \ + -v /path/to/local/config:/etc/dsfx/config \ + -v /path/to/local/data:/etx/dsfx/data \ + koti.casa/numenorlabs/dsfx:latest +``` diff --git a/internal/lib/disk/default.go b/internal/lib/disk/default.go index 006e96d..24efba7 100644 --- a/internal/lib/disk/default.go +++ b/internal/lib/disk/default.go @@ -36,6 +36,18 @@ func (d *osDisk) Mkdir(name string, perm fs.FileMode) error { return err } +// MkdirAll implements Disk. +// This creates a directory and any necessary parent directories. +// If the directory already exists, it does nothing. +// It returns an error if the directory cannot be created. +func (d *osDisk) MkdirAll(name string, perm fs.FileMode) error { + ts := time.Now() + err := os.MkdirAll(name, perm) + el := time.Now().Sub(ts).String() + d.logger.Debug("(io) disk.MkdirAll", "duration", el) + return err +} + // Open implements Disk. func (d *osDisk) Open(name string) (File, error) { ts := time.Now() diff --git a/internal/lib/disk/disk.go b/internal/lib/disk/disk.go index d37ae47..50660f8 100644 --- a/internal/lib/disk/disk.go +++ b/internal/lib/disk/disk.go @@ -21,6 +21,8 @@ type File interface { type Disk interface { // Mkdir creates a directory with the specified name and permissions. Mkdir(name string, perm fs.FileMode) error + // MkdirAll creates a directory and any necessary parent directories. + MkdirAll(name string, perm fs.FileMode) error // Create creates a new file with the specified name and returns a File interface. Create(name string) (File, error) // Stat retrieves the file information for the specified name, returning @@ -33,11 +35,12 @@ type Disk interface { } type MockDisk struct { - MkdirFunc func(name string, perm fs.FileMode) error - CreateFunc func(name string) (File, error) - StatFunc func(name string) (fs.FileInfo, error) - OpenFunc func(name string) (File, error) - RemoveFunc func(name string) error + MkdirFunc func(name string, perm fs.FileMode) error + MkdirAllFunc func(name string, perm fs.FileMode) error + CreateFunc func(name string) (File, error) + StatFunc func(name string) (fs.FileInfo, error) + OpenFunc func(name string) (File, error) + RemoveFunc func(name string) error } var _ Disk = &MockDisk{} @@ -49,6 +52,13 @@ func (t *MockDisk) Mkdir(name string, perm fs.FileMode) error { return nil } +func (t *MockDisk) MkdirAll(name string, perm fs.FileMode) error { + if t.MkdirAllFunc != nil { + return t.MkdirAllFunc(name, perm) + } + return nil +} + func (t *MockDisk) Create(name string) (File, error) { if t.CreateFunc != nil { return t.CreateFunc(name) diff --git a/internal/lib/storage/scoped/scoped.go b/internal/lib/storage/scoped/scoped.go index 851b23c..faa2d3f 100644 --- a/internal/lib/storage/scoped/scoped.go +++ b/internal/lib/storage/scoped/scoped.go @@ -38,6 +38,11 @@ func (s *scoped) Mkdir(name string, perm fs.FileMode) error { return s.disk.Mkdir(s.path(name), perm) } +// MkdirAll implements disk.Disk. +func (s *scoped) MkdirAll(name string, perm fs.FileMode) error { + return s.disk.MkdirAll(s.path(name), perm) +} + // Open implements disk.Disk. func (s *scoped) Open(name string) (disk.File, error) { return s.disk.Open(s.path(name)) diff --git a/internal/peer/node/node.go b/internal/peer/node/node.go index 7076f68..fab6ca6 100644 --- a/internal/peer/node/node.go +++ b/internal/peer/node/node.go @@ -14,83 +14,63 @@ import ( "koti.casa/numenor-labs/dsfx/internal/lib/disk" "koti.casa/numenor-labs/dsfx/internal/lib/logging" "koti.casa/numenor-labs/dsfx/internal/lib/network" + "koti.casa/numenor-labs/dsfx/internal/lib/storage/scoped" "koti.casa/numenor-labs/dsfx/internal/lib/system" "koti.casa/numenor-labs/dsfx/internal/peer/conf" ) type Node struct { - config disk.Disk - system system.System - conf conf.Conf + disk disk.Disk + system system.System + config scoped.StorageScope + storage scoped.StorageScope + conf conf.Conf } -func New(config disk.Disk, system system.System) *Node { +func New(disk disk.Disk, system system.System) *Node { conf := conf.FromSystem(system) - return &Node{config, system, conf} + + config := scoped.New(disk, conf.ConfigDir) + storage := scoped.New(disk, conf.StorageDir) + + return &Node{disk, system, config, storage, conf} } func (a *Node) Run(ctx context.Context) error { opts := &slog.HandlerOptions{ - AddSource: false, + AddSource: true, Level: slog.LevelDebug, } logger := slog.New(slog.NewJSONHandler(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) - ki := &KeyInit{disk: a.config} - - // Check if the key file exists and is not empty - hasKey, err := ki.Has() + err := a.disk.MkdirAll(a.conf.ConfigDir, 0755) if err != nil { - logger.ErrorContext(ctx, "failed to check key file", slog.Any("error", err)) + logger.ErrorContext(ctx, "failed to create config dir", slog.Any("error", err)) return err } - if !hasKey { - logger.InfoContext(ctx, "key file does not exist or is empty, generating new key") - err = ki.Init() - if err != nil { - logger.ErrorContext(ctx, "failed to initialize key file", slog.Any("error", err)) - return err - } + err = a.disk.MkdirAll(a.conf.StorageDir, 0755) + if err != nil { + logger.ErrorContext(ctx, "failed to create storage dir", slog.Any("error", err)) + return err } - // Read the key file - id, err := ki.Read() + id, err := a.loadIdentity() if err != nil { logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err)) return err } - admins := []string{} - ai := &AdminInit{disk: a.config} - - // Check if the admins file exists and is not empty - hasAdmins, err := ai.Has() + _, err = a.loadAdmins() if err != nil { - logger.ErrorContext(ctx, "failed to check admins file", slog.Any("error", err)) + logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err)) return err } - if hasAdmins { - logger.InfoContext(ctx, "admins file exists, reading admins") - admins, err = ai.Read() - if err != nil { - logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err)) - return err - } - logger.InfoContext(ctx, "loaded admins", slog.Any("admins", admins)) - } else { - logger.WarnContext(ctx, "admins file does not exist or is empty, no admins will be loaded") - } - tcpAddrRaw := net.JoinHostPort(a.conf.Host, a.conf.Port) tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw) if err != nil { @@ -118,27 +98,25 @@ func (a *Node) Run(ctx context.Context) error { } } -func handleConnection(ctx context.Context, conn net.Conn) error { - defer conn.Close() - - logger := logging.FromContext(ctx) - - msg := make([]byte, 1024) - n, err := conn.Read(msg) +// loadAdmins ... +func (c *Node) loadAdmins() ([]string, error) { + hasKeyFile, err := c.hasAdminsFile() if err != nil { - logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err)) - return err + return nil, fmt.Errorf("failed to check for admins file: %w", err) } - logger.InfoContext(ctx, "received msg", slog.Int("bytes", n)) + if !hasKeyFile { + if err := c.createAdminsFile(); err != nil { + return nil, fmt.Errorf("failed to create admins file: %w", err) + } + } - return nil + return c.readAdminsFile() } -type KeyInit struct{ disk disk.Disk } - -func (ki *KeyInit) Has() (bool, error) { - f, err := ki.disk.Open("key") +// hasAdminsFile ... +func (c *Node) hasAdminsFile() (bool, error) { + f, err := c.config.Open("admins") if err != nil { if os.IsNotExist(err) { return false, nil @@ -147,16 +125,82 @@ func (ki *KeyInit) Has() (bool, error) { } defer f.Close() - stats, err := f.Stat() - if err != nil { - return false, err - } - - return stats.Size() > 0, nil + return true, nil } -func (ki *KeyInit) Init() error { - f, err := ki.disk.Create("key") +// createAdminsFile ... +func (c *Node) createAdminsFile() error { + f, err := c.config.Create("admins") + if err != nil { + return err + } + defer f.Close() + + return nil +} + +// readAdminsFile ... +func (c *Node) readAdminsFile() ([]string, error) { + f, err := c.config.Open("admins") + if err != nil { + return nil, err + } + defer f.Close() + + keyRaw := make([]byte, 0) + _, err = f.Read(keyRaw) + if err != nil { + return nil, err + } + + lines := strings.SplitSeq(string(keyRaw), "\n") + + nonEmptyLines := []string{} + for line := range lines { + if strings.TrimSpace(line) != "" { + nonEmptyLines = append(nonEmptyLines, line) + } + } + + return nonEmptyLines, nil +} + +// loadIdentity loads the private key from the key file. If the key file does not +// exist, it creates a new key file with a generated private key and returns it. +func (c *Node) loadIdentity() (ed25519.PrivateKey, error) { + hasKeyFile, err := c.hasKeyFile() + if err != nil { + return nil, err + } + + if hasKeyFile { + return c.readKeyFile() + } + + if err := c.createKeyFile(); err != nil { + return nil, err + } + + return c.readKeyFile() +} + +// hasKeyFile checks if the key file exists in the disk storage. +func (c *Node) hasKeyFile() (bool, error) { + f, err := c.config.Open("key") + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + defer f.Close() + + return true, nil +} + +// createKeyFile creates a new key file with a generated private key. +func (c *Node) createKeyFile() error { + f, err := c.config.Create("key") if err != nil { return err } @@ -175,8 +219,9 @@ func (ki *KeyInit) Init() error { return nil } -func (ki *KeyInit) Read() (ed25519.PrivateKey, error) { - f, err := ki.disk.Open("key") +// readKeyFile reads the private key from the key file and returns it as an ed25519.PrivateKey. +func (c *Node) readKeyFile() (ed25519.PrivateKey, error) { + f, err := c.config.Open("key") if err != nil { return nil, err } @@ -194,51 +239,19 @@ func (ki *KeyInit) Read() (ed25519.PrivateKey, error) { return ed25519.PrivateKey(keyRaw), nil } -type AdminInit struct { - disk disk.Disk -} - -func (ai *AdminInit) Has() (bool, error) { - f, err := ai.disk.Open("admins") - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - defer f.Close() - - stats, err := f.Stat() - if err != nil { - return false, err - } - - return stats.Size() > 0, nil -} - -func (ai *AdminInit) Read() ([]string, error) { - f, err := ai.disk.Open("admins") - if err != nil { - return nil, err - } - defer f.Close() - - adminsRaw := make([]byte, 0) - n, err := f.Read(adminsRaw) - if err != nil { - return nil, err - } - if n == 0 { - return nil, fmt.Errorf("admins file is empty") - } - - rawAdmins := strings.Split(string(adminsRaw), "\n") - var admins []string - for _, admin := range rawAdmins { - if admin != "" { - admins = append(admins, admin) - } - } - - return admins, nil +func handleConnection(ctx context.Context, conn net.Conn) error { + defer conn.Close() + + logger := logging.FromContext(ctx) + + msg := make([]byte, 1024) + n, err := conn.Read(msg) + if err != nil { + logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err)) + return err + } + + logger.InfoContext(ctx, "received msg", slog.Int("bytes", n)) + + return nil } diff --git a/internal/sim/disk.go b/internal/sim/disk.go index 0b6c448..2eca2cc 100644 --- a/internal/sim/disk.go +++ b/internal/sim/disk.go @@ -202,6 +202,29 @@ func (sd *SimDisk) Mkdir(name string, perm fs.FileMode) error { return nil } +// MkdirAll creates a directory and any necessary parent directories in the simulated file system. +// It behaves like os.MkdirAll. +// If the directory already exists, it does nothing. +func (sd *SimDisk) MkdirAll(name string, perm fs.FileMode) error { + if err := sd.simulateOp(); err != nil { + return err + } + + sd.mu.Lock() + defer sd.mu.Unlock() + + if _, exists := sd.entries[name]; exists { + return errors.New("directory already exists") + } + sd.entries[name] = &simEntry{ + name: name, + isDir: true, + perm: perm, + modTime: time.Now(), + } + return nil +} + // Create creates (or truncates) a file in the simulated file system and returns a writable file. func (sd *SimDisk) Create(name string) (disk.File, error) { if err := sd.simulateOp(); err != nil {