2025-03-22 13:38:52 -04:00
|
|
|
package peer
|
2025-03-21 20:23:15 -04:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/ed25519"
|
|
|
|
"encoding/base64"
|
2025-03-23 10:02:19 -04:00
|
|
|
"errors"
|
2025-03-24 13:57:42 -04:00
|
|
|
"flag"
|
2025-03-21 20:23:15 -04:00
|
|
|
"fmt"
|
2025-03-22 14:28:03 -04:00
|
|
|
"io"
|
2025-03-21 20:23:15 -04:00
|
|
|
"log/slog"
|
|
|
|
"net"
|
2025-03-23 10:02:19 -04:00
|
|
|
"os"
|
2025-03-21 20:23:15 -04:00
|
|
|
"strings"
|
|
|
|
|
2025-03-25 15:24:26 -04:00
|
|
|
"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"
|
2025-03-22 13:38:52 -04:00
|
|
|
)
|
2025-03-21 22:51:04 -04:00
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
const (
|
2025-03-23 10:17:45 -04:00
|
|
|
// EnvHost is the environment variable for the dsfx host.
|
|
|
|
EnvHost = "DSFX_HOST"
|
|
|
|
// EnvPort is the environment variable for the dsfx port.
|
|
|
|
EnvPort = "DSFX_PORT"
|
|
|
|
// EnvLogLevel is the environment variable for the dsfx log level.
|
|
|
|
EnvLogLevel = "DSFX_LOG_LEVEL"
|
|
|
|
// EnvDataDir is the environment variable for the dsfx storage directory.
|
|
|
|
EnvDataDir = "DSFX_DATA_DIR"
|
|
|
|
// EnvConfigDir is the environment variable for the dsfx configuration directory.
|
|
|
|
EnvConfigDir = "DSFX_CONFIG_DIR"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
2025-03-22 13:38:52 -04:00
|
|
|
// DefaultHost is the default host for the dsfxctl application.
|
|
|
|
DefaultHost = "0.0.0.0"
|
|
|
|
// DefaultPort is the default port for the dsfxctl application.
|
|
|
|
DefaultPort = "8000"
|
2025-03-23 10:17:45 -04:00
|
|
|
// DefaultDataDir is the default directory for the dsfx storage.
|
|
|
|
DefaultDataDir = "/etc/dsfx/data"
|
|
|
|
// DefaultConfigDir is the default directory for the dsfx configuration.
|
|
|
|
DefaultConfigDir = "/etc/dsfx/config"
|
2025-03-21 20:23:15 -04:00
|
|
|
)
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
// Conf holds the configuration for the dsfxctl application.
|
|
|
|
type Conf struct {
|
2025-03-23 10:02:19 -04:00
|
|
|
LogLevel string
|
2025-03-22 13:38:52 -04:00
|
|
|
// Directories
|
2025-03-23 10:17:45 -04:00
|
|
|
ConfigDir string
|
|
|
|
DataDir string
|
2025-03-22 13:38:52 -04:00
|
|
|
// Networking
|
|
|
|
Host string
|
|
|
|
Port string
|
|
|
|
}
|
|
|
|
|
2025-03-23 10:02:19 -04:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Peer ...
|
2025-03-22 13:38:52 -04:00
|
|
|
type Peer struct {
|
2025-03-22 13:06:51 -04:00
|
|
|
disk disk.Disk
|
|
|
|
system system.System
|
|
|
|
config scoped.StorageScope
|
|
|
|
storage scoped.StorageScope
|
2025-03-22 13:38:52 -04:00
|
|
|
conf Conf
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-23 10:02:19 -04:00
|
|
|
// New ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func New(disk disk.Disk, system system.System) *Peer {
|
2025-03-24 13:57:42 -04:00
|
|
|
flagConfig := flag.String("configDir", "/etc/dsfx/config", "Path to the configuration directory")
|
|
|
|
flagData := flag.String("dataDir", "/etc/dsfx/data", "Path to the data directory")
|
|
|
|
flagLogLevel := flag.String("logLevel", "info", "The log level (debug, info, warn, error)")
|
|
|
|
flagHost := flag.String("host", "0.0.0.0", "The host to bind to")
|
|
|
|
flagPort := flag.String("port", "8000", "The port to bind to")
|
|
|
|
|
|
|
|
if !flag.Parsed() {
|
|
|
|
flag.Parse()
|
|
|
|
}
|
|
|
|
conf := Conf{
|
|
|
|
LogLevel: *flagLogLevel,
|
|
|
|
ConfigDir: *flagConfig,
|
|
|
|
DataDir: *flagData,
|
|
|
|
Host: *flagHost,
|
|
|
|
Port: *flagPort,
|
|
|
|
}
|
2025-03-22 13:06:51 -04:00
|
|
|
|
|
|
|
config := scoped.New(disk, conf.ConfigDir)
|
2025-03-23 10:17:45 -04:00
|
|
|
storage := scoped.New(disk, conf.DataDir)
|
2025-03-22 13:06:51 -04:00
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
return &Peer{disk, system, config, storage, conf}
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-23 10:02:19 -04:00
|
|
|
// Run ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) Run(ctx context.Context) error {
|
2025-03-21 20:23:15 -04:00
|
|
|
opts := &slog.HandlerOptions{
|
2025-03-22 13:18:27 -04:00
|
|
|
AddSource: false,
|
2025-03-23 10:02:19 -04:00
|
|
|
Level: p.conf.SlogLevel(),
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
2025-03-22 13:38:52 -04:00
|
|
|
logger := slog.New(slog.NewTextHandler(p.system.Stdout(), opts))
|
2025-03-21 20:23:15 -04:00
|
|
|
|
|
|
|
slog.SetDefault(logger)
|
|
|
|
ctx = logging.WithContext(ctx, logger)
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
err := p.disk.MkdirAll(p.conf.ConfigDir, 0755)
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
logger.ErrorContext(ctx, "failed to create config dir", slog.Any("error", err))
|
2025-03-21 20:23:15 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-03-23 10:17:45 -04:00
|
|
|
err = p.disk.MkdirAll(p.conf.DataDir, 0755)
|
2025-03-22 13:06:51 -04:00
|
|
|
if err != nil {
|
|
|
|
logger.ErrorContext(ctx, "failed to create storage dir", slog.Any("error", err))
|
|
|
|
return err
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
id, err := p.loadIdentity()
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
admins, err := p.loadAdmins()
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err))
|
2025-03-21 20:23:15 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:18:27 -04:00
|
|
|
if len(admins) == 0 {
|
|
|
|
logger.WarnContext(ctx, "no admins found", slog.String("admins", "none"))
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
tcpAddrRaw := net.JoinHostPort(p.conf.Host, p.conf.Port)
|
2025-03-21 20:23:15 -04:00
|
|
|
tcpAddr, err := net.ResolveTCPAddr("tcp", tcpAddrRaw)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
addr := network.FromTCPAddr(tcpAddr, id.Public().(ed25519.PublicKey))
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Listener
|
|
|
|
|
|
|
|
listener, err := network.Listen(ctx, id, addr)
|
|
|
|
if err != nil {
|
|
|
|
logger.ErrorContext(ctx, "listener error", slog.Any("error", err))
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:18:27 -04:00
|
|
|
logger.InfoContext(ctx, "serving", slog.String("address", addr.String()))
|
|
|
|
|
2025-03-21 20:23:15 -04:00
|
|
|
for {
|
|
|
|
conn, err := listener.Accept()
|
|
|
|
if err != nil {
|
|
|
|
logger.ErrorContext(ctx, "accept failure", slog.Any("error", err))
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
go p.handleConnection(ctx, conn)
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// loadAdmins ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) loadAdmins() ([]string, error) {
|
2025-03-24 12:41:56 -04:00
|
|
|
hasAdminsFile, err := p.hasAdminsFile()
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil, fmt.Errorf("failed to check for admins file: %w", err)
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-24 12:41:56 -04:00
|
|
|
if !hasAdminsFile {
|
2025-03-22 13:38:52 -04:00
|
|
|
if err := p.createAdminsFile(); err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil, fmt.Errorf("failed to create admins file: %w", err)
|
|
|
|
}
|
|
|
|
}
|
2025-03-21 20:23:15 -04:00
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
return p.readAdminsFile()
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// hasAdminsFile ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) hasAdminsFile() (bool, error) {
|
|
|
|
f, err := p.config.Open("admins")
|
2025-03-23 10:02:19 -04:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
return false, nil // Key file does not exist
|
|
|
|
}
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createAdminsFile ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) createAdminsFile() error {
|
|
|
|
f, err := p.config.Create("admins")
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return err
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
2025-03-22 13:06:51 -04:00
|
|
|
defer f.Close()
|
2025-03-21 20:23:15 -04:00
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// readAdminsFile ...
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) readAdminsFile() ([]string, error) {
|
|
|
|
f, err := p.config.Open("admins")
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil, err
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2025-03-24 12:41:56 -04:00
|
|
|
keyRaw, err := io.ReadAll(f)
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil, err
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
lines := strings.SplitSeq(string(keyRaw), "\n")
|
|
|
|
|
|
|
|
nonEmptyLines := []string{}
|
|
|
|
for line := range lines {
|
|
|
|
if strings.TrimSpace(line) != "" {
|
|
|
|
nonEmptyLines = append(nonEmptyLines, line)
|
|
|
|
}
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return nonEmptyLines, nil
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// 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.
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) loadIdentity() (ed25519.PrivateKey, error) {
|
|
|
|
hasKeyFile, err := p.hasKeyFile()
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
if hasKeyFile {
|
2025-03-22 13:38:52 -04:00
|
|
|
return p.readKeyFile()
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
if err := p.createKeyFile(); err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil, err
|
|
|
|
}
|
2025-03-21 20:23:15 -04:00
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
return p.readKeyFile()
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// hasKeyFile checks if the key file exists in the disk storage.
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) hasKeyFile() (bool, error) {
|
2025-03-22 14:28:03 -04:00
|
|
|
f, err := p.config.Open("ed25519.key")
|
2025-03-23 10:02:19 -04:00
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
|
|
return false, nil // Key file does not exist
|
|
|
|
}
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createKeyFile creates a new key file with a generated private key.
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) createKeyFile() error {
|
2025-03-22 14:28:03 -04:00
|
|
|
f, err := p.config.Create("ed25519.key")
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
2025-03-22 13:06:51 -04:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
privkey, err := identity.Generate()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
_, err = f.Write([]byte(base64.StdEncoding.EncodeToString(privkey)))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return nil
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
// readKeyFile reads the private key from the key file and returns it as an ed25519.PrivateKey.
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) readKeyFile() (ed25519.PrivateKey, error) {
|
2025-03-22 14:28:03 -04:00
|
|
|
f, err := p.config.Open("ed25519.key")
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
2025-03-22 14:28:03 -04:00
|
|
|
keyRawBase64, err := io.ReadAll(f)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
keyRaw, err := base64.StdEncoding.DecodeString(string(keyRawBase64))
|
2025-03-21 20:23:15 -04:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2025-03-22 14:28:03 -04:00
|
|
|
if len(keyRaw) != ed25519.PrivateKeySize {
|
|
|
|
return nil, fmt.Errorf("key file is not the correct size: %d", len(keyRaw))
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
return ed25519.PrivateKey(keyRaw), nil
|
|
|
|
}
|
|
|
|
|
2025-03-22 13:38:52 -04:00
|
|
|
func (p *Peer) handleConnection(ctx context.Context, conn net.Conn) error {
|
2025-03-22 13:06:51 -04:00
|
|
|
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
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|
|
|
|
|
2025-03-22 13:06:51 -04:00
|
|
|
logger.InfoContext(ctx, "received msg", slog.Int("bytes", n))
|
|
|
|
|
|
|
|
return nil
|
2025-03-21 20:23:15 -04:00
|
|
|
}
|