mirror of
https://git.numenor-labs.us/dsfx.git
synced 2025-04-29 08:10:34 +00:00
Previously, the 'keyRaw' slice was being allocated with a length of 0. However, the 'Read()' function will only read up to the slice's length. Together, this means that we always read 0 bytes from the admins file. Now, we can use the 'io.ReadAll()' function to read all of the admins file before parsing.
350 lines
7.7 KiB
Go
350 lines
7.7 KiB
Go
package peer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
|
|
"koti.casa/numenor-labs/dsfx/internal/lib/crypto/identity"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
// 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 (
|
|
// 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"
|
|
// 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"
|
|
)
|
|
|
|
// Conf holds the configuration for the dsfxctl application.
|
|
type Conf struct {
|
|
LogLevel string
|
|
// Directories
|
|
ConfigDir string
|
|
DataDir string
|
|
// Networking
|
|
Host string
|
|
Port 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
|
|
}
|
|
}
|
|
|
|
func loadConfigFromSystem(sys system.System) Conf {
|
|
var c Conf
|
|
|
|
c.Host = sys.GetEnv(EnvHost)
|
|
if len(c.Host) == 0 {
|
|
c.Host = DefaultHost
|
|
}
|
|
|
|
c.Port = sys.GetEnv(EnvPort)
|
|
if len(c.Port) == 0 {
|
|
c.Port = DefaultPort
|
|
}
|
|
|
|
c.ConfigDir = sys.GetEnv(EnvConfigDir)
|
|
if len(c.ConfigDir) == 0 {
|
|
c.ConfigDir = DefaultConfigDir
|
|
}
|
|
|
|
c.DataDir = sys.GetEnv(EnvDataDir)
|
|
if len(c.DataDir) == 0 {
|
|
c.DataDir = DefaultDataDir
|
|
}
|
|
|
|
// defaults are handled by Conf.SlogLevel.
|
|
c.LogLevel = sys.GetEnv("DSFX_LOG_LEVEL")
|
|
|
|
return c
|
|
}
|
|
|
|
// Peer ...
|
|
type Peer struct {
|
|
disk disk.Disk
|
|
system system.System
|
|
config scoped.StorageScope
|
|
storage scoped.StorageScope
|
|
conf Conf
|
|
}
|
|
|
|
// New ...
|
|
func New(disk disk.Disk, system system.System) *Peer {
|
|
conf := loadConfigFromSystem(system)
|
|
|
|
config := scoped.New(disk, conf.ConfigDir)
|
|
storage := scoped.New(disk, conf.DataDir)
|
|
|
|
return &Peer{disk, system, config, storage, conf}
|
|
}
|
|
|
|
// Run ...
|
|
func (p *Peer) Run(ctx context.Context) error {
|
|
opts := &slog.HandlerOptions{
|
|
AddSource: false,
|
|
Level: p.conf.SlogLevel(),
|
|
}
|
|
logger := slog.New(slog.NewTextHandler(p.system.Stdout(), opts))
|
|
|
|
slog.SetDefault(logger)
|
|
ctx = logging.WithContext(ctx, logger)
|
|
|
|
err := p.disk.MkdirAll(p.conf.ConfigDir, 0755)
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "failed to create config dir", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
err = p.disk.MkdirAll(p.conf.DataDir, 0755)
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "failed to create storage dir", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
id, err := p.loadIdentity()
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "failed to read key file", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
admins, err := p.loadAdmins()
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "failed to read admins file", slog.Any("error", err))
|
|
return err
|
|
}
|
|
|
|
if len(admins) == 0 {
|
|
logger.WarnContext(ctx, "no admins found", slog.String("admins", "none"))
|
|
}
|
|
|
|
tcpAddrRaw := net.JoinHostPort(p.conf.Host, p.conf.Port)
|
|
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
|
|
}
|
|
|
|
logger.InfoContext(ctx, "serving", slog.String("address", addr.String()))
|
|
|
|
for {
|
|
conn, err := listener.Accept()
|
|
if err != nil {
|
|
logger.ErrorContext(ctx, "accept failure", slog.Any("error", err))
|
|
continue
|
|
}
|
|
|
|
go p.handleConnection(ctx, conn)
|
|
}
|
|
}
|
|
|
|
// loadAdmins ...
|
|
func (p *Peer) loadAdmins() ([]string, error) {
|
|
hasAdminsFile, err := p.hasAdminsFile()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to check for admins file: %w", err)
|
|
}
|
|
|
|
if !hasAdminsFile {
|
|
if err := p.createAdminsFile(); err != nil {
|
|
return nil, fmt.Errorf("failed to create admins file: %w", err)
|
|
}
|
|
}
|
|
|
|
return p.readAdminsFile()
|
|
}
|
|
|
|
// hasAdminsFile ...
|
|
func (p *Peer) hasAdminsFile() (bool, error) {
|
|
f, err := p.config.Open("admins")
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil // Key file does not exist
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer f.Close()
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// createAdminsFile ...
|
|
func (p *Peer) createAdminsFile() error {
|
|
f, err := p.config.Create("admins")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
// readAdminsFile ...
|
|
func (p *Peer) readAdminsFile() ([]string, error) {
|
|
f, err := p.config.Open("admins")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer f.Close()
|
|
|
|
keyRaw, err := io.ReadAll(f)
|
|
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 (p *Peer) loadIdentity() (ed25519.PrivateKey, error) {
|
|
hasKeyFile, err := p.hasKeyFile()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if hasKeyFile {
|
|
return p.readKeyFile()
|
|
}
|
|
|
|
if err := p.createKeyFile(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.readKeyFile()
|
|
}
|
|
|
|
// hasKeyFile checks if the key file exists in the disk storage.
|
|
func (p *Peer) hasKeyFile() (bool, error) {
|
|
f, err := p.config.Open("ed25519.key")
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false, nil // Key file does not exist
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer f.Close()
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// createKeyFile creates a new key file with a generated private key.
|
|
func (p *Peer) createKeyFile() error {
|
|
f, err := p.config.Create("ed25519.key")
|
|
if err != nil {
|
|
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
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readKeyFile reads the private key from the key file and returns it as an ed25519.PrivateKey.
|
|
func (p *Peer) readKeyFile() (ed25519.PrivateKey, error) {
|
|
f, err := p.config.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, 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
|
|
}
|
|
|
|
func (p *Peer) 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
|
|
}
|