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 (
"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)
}
}

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

View File

@ -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()

View File

@ -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
@ -34,6 +36,7 @@ type Disk interface {
type MockDisk struct {
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)
@ -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)

View File

@ -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))

View File

@ -14,82 +14,62 @@ 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
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()
err = a.disk.MkdirAll(a.conf.StorageDir, 0755)
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
}
}
// 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()
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()
_, err = a.loadAdmins()
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)
@ -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))
return nil
if !hasKeyFile {
if err := c.createAdminsFile(); err != nil {
return nil, fmt.Errorf("failed to create admins file: %w", err)
}
}
type KeyInit struct{ disk disk.Disk }
return c.readAdminsFile()
}
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()
return true, nil
}
// 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 stats.Size() > 0, nil
return true, nil
}
func (ki *KeyInit) Init() error {
f, err := ki.disk.Create("key")
// 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 handleConnection(ctx context.Context, conn net.Conn) error {
defer conn.Close()
func (ai *AdminInit) Has() (bool, error) {
f, err := ai.disk.Open("admins")
logger := logging.FromContext(ctx)
msg := make([]byte, 1024)
n, err := conn.Read(msg)
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
logger.ErrorContext(ctx, "failed to read from connection", slog.Any("error", err))
return err
}
return stats.Size() > 0, nil
}
logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))
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
return nil
}

View File

@ -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 {