fix(internal/peer): automatically create key and admin files

This commit is contained in:
Dustin Stiles 2025-03-22 13:06:51 -04:00
parent 336b371449
commit a74cdb8b02
Signed by: duwstiles
GPG Key ID: BCD9912EC231FC87
7 changed files with 203 additions and 135 deletions

View File

@ -2,30 +2,17 @@ package main
import ( import (
"context" "context"
"log/slog"
"koti.casa/numenor-labs/dsfx/internal/lib/disk" "koti.casa/numenor-labs/dsfx/internal/lib/disk"
"koti.casa/numenor-labs/dsfx/internal/lib/system" "koti.casa/numenor-labs/dsfx/internal/lib/system"
"koti.casa/numenor-labs/dsfx/internal/peer/node" "koti.casa/numenor-labs/dsfx/internal/peer/node"
"koti.casa/numenor-labs/dsfx/internal/lib/storage/scoped"
) )
func main() { func main() {
ctx := context.Background() n := node.New(disk.Default(), system.Default())
sys := system.Default() err := n.Run(context.Background())
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)
if err != nil { if err != nil {
// Log the error and exit with a non-zero status code. panic(err)
slog.Error("Error running dsfxnode", slog.Any("error", err))
sys.Exit(1)
} }
} }

View File

@ -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_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_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 | | 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
```

View File

@ -36,6 +36,18 @@ func (d *osDisk) Mkdir(name string, perm fs.FileMode) error {
return err 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. // Open implements Disk.
func (d *osDisk) Open(name string) (File, error) { func (d *osDisk) Open(name string) (File, error) {
ts := time.Now() ts := time.Now()

View File

@ -21,6 +21,8 @@ type File interface {
type Disk interface { type Disk interface {
// Mkdir creates a directory with the specified name and permissions. // Mkdir creates a directory with the specified name and permissions.
Mkdir(name string, perm fs.FileMode) error 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 creates a new file with the specified name and returns a File interface.
Create(name string) (File, error) Create(name string) (File, error)
// Stat retrieves the file information for the specified name, returning // Stat retrieves the file information for the specified name, returning
@ -34,6 +36,7 @@ type Disk interface {
type MockDisk struct { type MockDisk struct {
MkdirFunc func(name string, perm fs.FileMode) error MkdirFunc func(name string, perm fs.FileMode) error
MkdirAllFunc func(name string, perm fs.FileMode) error
CreateFunc func(name string) (File, error) CreateFunc func(name string) (File, error)
StatFunc func(name string) (fs.FileInfo, error) StatFunc func(name string) (fs.FileInfo, error)
OpenFunc func(name string) (File, error) OpenFunc func(name string) (File, error)
@ -49,6 +52,13 @@ func (t *MockDisk) Mkdir(name string, perm fs.FileMode) error {
return nil 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) { func (t *MockDisk) Create(name string) (File, error) {
if t.CreateFunc != nil { if t.CreateFunc != nil {
return t.CreateFunc(name) return t.CreateFunc(name)

View File

@ -38,6 +38,11 @@ func (s *scoped) Mkdir(name string, perm fs.FileMode) error {
return s.disk.Mkdir(s.path(name), perm) 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. // Open implements disk.Disk.
func (s *scoped) Open(name string) (disk.File, error) { func (s *scoped) Open(name string) (disk.File, error) {
return s.disk.Open(s.path(name)) return s.disk.Open(s.path(name))

View File

@ -14,82 +14,62 @@ import (
"koti.casa/numenor-labs/dsfx/internal/lib/disk" "koti.casa/numenor-labs/dsfx/internal/lib/disk"
"koti.casa/numenor-labs/dsfx/internal/lib/logging" "koti.casa/numenor-labs/dsfx/internal/lib/logging"
"koti.casa/numenor-labs/dsfx/internal/lib/network" "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/lib/system"
"koti.casa/numenor-labs/dsfx/internal/peer/conf" "koti.casa/numenor-labs/dsfx/internal/peer/conf"
) )
type Node struct { type Node struct {
config disk.Disk disk disk.Disk
system system.System system system.System
config scoped.StorageScope
storage scoped.StorageScope
conf conf.Conf 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) 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 { func (a *Node) Run(ctx context.Context) error {
opts := &slog.HandlerOptions{ opts := &slog.HandlerOptions{
AddSource: false, AddSource: true,
Level: slog.LevelDebug, Level: slog.LevelDebug,
} }
logger := slog.New(slog.NewJSONHandler(a.system.Stdout(), opts)) 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) slog.SetDefault(logger)
ctx = logging.WithContext(ctx, logger) ctx = logging.WithContext(ctx, logger)
ki := &KeyInit{disk: a.config} err := a.disk.MkdirAll(a.conf.ConfigDir, 0755)
// Check if the key file exists and is not empty
hasKey, err := ki.Has()
if err != nil { 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 return err
} }
if !hasKey { err = a.disk.MkdirAll(a.conf.StorageDir, 0755)
logger.InfoContext(ctx, "key file does not exist or is empty, generating new key")
err = ki.Init()
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to initialize key file", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to create storage dir", slog.Any("error", err))
return err return err
} }
}
// Read the key file id, err := a.loadIdentity()
id, err := ki.Read()
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
return err return err
} }
admins := []string{} _, err = a.loadAdmins()
ai := &AdminInit{disk: a.config}
// Check if the admins file exists and is not empty
hasAdmins, err := ai.Has()
if err != nil {
logger.ErrorContext(ctx, "failed to check 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 { if err != nil {
logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err)) logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err))
return 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) tcpAddrRaw := net.JoinHostPort(a.conf.Host, a.conf.Port)
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw) tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
@ -118,27 +98,25 @@ func (a *Node) Run(ctx context.Context) error {
} }
} }
func handleConnection(ctx context.Context, conn net.Conn) error { // loadAdmins ...
defer conn.Close() func (c *Node) loadAdmins() ([]string, error) {
hasKeyFile, err := c.hasAdminsFile()
logger := logging.FromContext(ctx)
msg := make([]byte, 1024)
n, err := conn.Read(msg)
if err != nil { if err != nil {
logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err)) return nil, fmt.Errorf("failed to check for admins file: %w", err)
return 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 } // hasAdminsFile ...
func (c *Node) hasAdminsFile() (bool, error) {
func (ki *KeyInit) Has() (bool, error) { f, err := c.config.Open("admins")
f, err := ki.disk.Open("key")
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return false, nil return false, nil
@ -147,16 +125,82 @@ func (ki *KeyInit) Has() (bool, error) {
} }
defer f.Close() defer f.Close()
stats, err := f.Stat() return true, nil
if err != nil {
return false, err
}
return stats.Size() > 0, nil
} }
func (ki *KeyInit) Init() error { // createAdminsFile ...
f, err := ki.disk.Create("key") 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 { if err != nil {
return err return err
} }
@ -175,8 +219,9 @@ func (ki *KeyInit) Init() error {
return nil return nil
} }
func (ki *KeyInit) Read() (ed25519.PrivateKey, error) { // readKeyFile reads the private key from the key file and returns it as an ed25519.PrivateKey.
f, err := ki.disk.Open("key") func (c *Node) readKeyFile() (ed25519.PrivateKey, error) {
f, err := c.config.Open("key")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -194,51 +239,19 @@ func (ki *KeyInit) Read() (ed25519.PrivateKey, error) {
return ed25519.PrivateKey(keyRaw), nil return ed25519.PrivateKey(keyRaw), nil
} }
type AdminInit struct { func handleConnection(ctx context.Context, conn net.Conn) error {
disk disk.Disk defer conn.Close()
}
logger := logging.FromContext(ctx)
func (ai *AdminInit) Has() (bool, error) {
f, err := ai.disk.Open("admins") msg := make([]byte, 1024)
if err != nil { n, err := conn.Read(msg)
if os.IsNotExist(err) { if err != nil {
return false, nil logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err))
} return err
return false, err }
}
defer f.Close() logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))
stats, err := f.Stat() return nil
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
} }

View File

@ -202,6 +202,29 @@ func (sd *SimDisk) Mkdir(name string, perm fs.FileMode) error {
return nil 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. // 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) { func (sd *SimDisk) Create(name string) (disk.File, error) {
if err := sd.simulateOp(); err != nil { if err := sd.simulateOp(); err != nil {