package peer import ( "context" "crypto/ed25519" "encoding/base64" "errors" "flag" "fmt" "io" "log/slog" "net" "os" "strings" "git.numenor-labs.us/dsfx/internal/lib/crypto/identity" "git.numenor-labs.us/dsfx/internal/lib/disk" "git.numenor-labs.us/dsfx/internal/lib/logging" "git.numenor-labs.us/dsfx/internal/lib/network" "git.numenor-labs.us/dsfx/internal/lib/storage/scoped" "git.numenor-labs.us/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 } } // 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 { 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, } 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 }