Files
Reticulum-Go/pkg/config/config.go
Sudo-Ivan 1d3a969742
Some checks failed
Bearer / scan (push) Successful in 9s
Go Build Multi-Platform / build (amd64, linux) (push) Successful in 42s
Go Build Multi-Platform / build (amd64, darwin) (push) Successful in 44s
Go Build Multi-Platform / build (arm, freebsd) (push) Successful in 41s
Go Build Multi-Platform / build (arm, windows) (push) Successful in 39s
Go Build Multi-Platform / build (arm64, windows) (push) Successful in 1m8s
Go Build Multi-Platform / build (wasm, js) (push) Successful in 1m6s
TinyGo Build / tinygo-build (tinygo-wasm, tinygo-wasm, reticulum-go.wasm, wasm) (pull_request) Failing after 1m2s
TinyGo Build / tinygo-build (tinygo-build, tinygo-default, reticulum-go-tinygo, ) (pull_request) Failing after 1m4s
Go Revive Lint / lint (push) Successful in 1m4s
Go Test Multi-Platform / Test (ubuntu-latest, arm64) (push) Successful in 1m24s
Run Gosec / tests (push) Successful in 1m29s
Go Test Multi-Platform / Test (ubuntu-latest, amd64) (push) Successful in 2m31s
Go Build Multi-Platform / build (amd64, freebsd) (push) Successful in 9m28s
Go Build Multi-Platform / build (arm, linux) (push) Successful in 9m28s
Go Build Multi-Platform / build (amd64, windows) (push) Successful in 9m30s
Go Build Multi-Platform / build (arm64, darwin) (push) Successful in 9m27s
Go Build Multi-Platform / build (arm64, linux) (push) Successful in 9m26s
Go Build Multi-Platform / build (arm64, freebsd) (push) Successful in 9m29s
Go Build Multi-Platform / Create Release (push) Has been skipped
chore: add SPDX license identifier and copyright notice
2025-12-31 20:44:58 -06:00

273 lines
6.7 KiB
Go

// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
package config
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
)
type Config struct {
Identity struct {
Name string
StoragePath string
}
Interfaces []struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}
Transport struct {
AnnounceInterval int
PathRequestTimeout int
MaxHops int
BitrateLimit int64
}
Logging struct {
Level string
File string
}
}
func LoadConfig(path string) (*Config, error) {
// bearer:disable go_gosec_filesystem_filereadtaint
file, err := os.Open(path) // #nosec G304
if err != nil {
return nil, err
}
defer file.Close()
cfg := &Config{}
scanner := bufio.NewScanner(file)
var currentSection string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
// Skip comments and empty lines
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Handle sections
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
currentSection = strings.Trim(line, "[]")
// If this is an interface section, append new interface
if strings.HasPrefix(currentSection, "interface ") {
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{})
}
continue
}
// Parse key-value pairs
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
switch currentSection {
case "identity":
switch key {
case "name":
cfg.Identity.Name = value
case "storage_path":
cfg.Identity.StoragePath = value
}
case "transport":
switch key {
case "announce_interval":
cfg.Transport.AnnounceInterval, _ = strconv.Atoi(value)
case "path_request_timeout":
cfg.Transport.PathRequestTimeout, _ = strconv.Atoi(value)
case "max_hops":
cfg.Transport.MaxHops, _ = strconv.Atoi(value)
case "bitrate_limit":
cfg.Transport.BitrateLimit, _ = strconv.ParseInt(value, 10, 64)
}
case "logging":
switch key {
case "level":
cfg.Logging.Level = value
case "file":
cfg.Logging.File = value
}
default:
// Handle interface sections
if strings.HasPrefix(currentSection, "interface ") && len(cfg.Interfaces) > 0 {
iface := &cfg.Interfaces[len(cfg.Interfaces)-1]
switch key {
case "name":
iface.Name = value
case "type":
iface.Type = value
case "enabled":
iface.Enabled = value == "true"
case "listen_port":
iface.ListenPort, _ = strconv.Atoi(value)
case "listen_ip":
iface.ListenIP = value
case "kiss_framing":
iface.KissFraming = value == "true"
case "i2p_tunneled":
iface.I2PTunneled = value == "true"
}
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return cfg, nil
}
func SaveConfig(cfg *Config, path string) error {
var builder strings.Builder
// Write Identity section
builder.WriteString("[identity]\n")
builder.WriteString(fmt.Sprintf("name = %s\n", cfg.Identity.Name))
builder.WriteString(fmt.Sprintf("storage_path = %s\n\n", cfg.Identity.StoragePath))
// Write Transport section
builder.WriteString("[transport]\n")
builder.WriteString(fmt.Sprintf("announce_interval = %d\n", cfg.Transport.AnnounceInterval))
builder.WriteString(fmt.Sprintf("path_request_timeout = %d\n", cfg.Transport.PathRequestTimeout))
builder.WriteString(fmt.Sprintf("max_hops = %d\n", cfg.Transport.MaxHops))
builder.WriteString(fmt.Sprintf("bitrate_limit = %d\n\n", cfg.Transport.BitrateLimit))
// Write Logging section
builder.WriteString("[logging]\n")
builder.WriteString(fmt.Sprintf("level = %s\n", cfg.Logging.Level))
builder.WriteString(fmt.Sprintf("file = %s\n\n", cfg.Logging.File))
// Write Interface sections
for _, iface := range cfg.Interfaces {
builder.WriteString(fmt.Sprintf("[interface %s]\n", iface.Name))
builder.WriteString(fmt.Sprintf("type = %s\n", iface.Type))
builder.WriteString(fmt.Sprintf("enabled = %v\n", iface.Enabled))
builder.WriteString(fmt.Sprintf("listen_port = %d\n", iface.ListenPort))
builder.WriteString(fmt.Sprintf("listen_ip = %s\n", iface.ListenIP))
builder.WriteString(fmt.Sprintf("kiss_framing = %v\n", iface.KissFraming))
builder.WriteString(fmt.Sprintf("i2p_tunneled = %v\n\n", iface.I2PTunneled))
}
return os.WriteFile(path, []byte(builder.String()), 0600) // #nosec G306
}
func GetConfigDir() string {
homeDir, err := os.UserHomeDir()
if err != nil {
// Fallback to current directory if home directory cannot be determined
return ".reticulum-go"
}
return filepath.Join(homeDir, ".reticulum-go")
}
func GetDefaultConfigPath() string {
return filepath.Join(GetConfigDir(), "config")
}
func EnsureConfigDir() error {
configDir := GetConfigDir()
return os.MkdirAll(configDir, 0700) // #nosec G301
}
func InitConfig() (*Config, error) {
// Ensure config directory exists
if err := EnsureConfigDir(); err != nil {
return nil, fmt.Errorf("failed to create config directory: %v", err)
}
configPath := GetDefaultConfigPath()
// Check if config file exists
if _, err := os.Stat(configPath); os.IsNotExist(err) {
// Create default config
cfg := &Config{}
// Set default values
cfg.Identity.Name = "reticulum-node"
cfg.Identity.StoragePath = filepath.Join(GetConfigDir(), "storage")
cfg.Transport.AnnounceInterval = 300
cfg.Transport.PathRequestTimeout = 15
cfg.Transport.MaxHops = 8
cfg.Transport.BitrateLimit = 1000000
cfg.Logging.Level = "info"
cfg.Logging.File = filepath.Join(GetConfigDir(), "reticulum.log")
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{
Name: "Local UDP",
Type: "UDPInterface",
Enabled: true,
ListenPort: 37697,
ListenIP: "0.0.0.0",
})
cfg.Interfaces = append(cfg.Interfaces, struct {
Name string
Type string
Enabled bool
ListenPort int
ListenIP string
KissFraming bool
I2PTunneled bool
}{
Name: "Auto Discovery",
Type: "AutoInterface",
Enabled: true,
ListenPort: 29717,
})
// Save default config
if err := SaveConfig(cfg, configPath); err != nil {
return nil, fmt.Errorf("failed to save default config: %v", err)
}
}
// Load config
cfg, err := LoadConfig(configPath)
if err != nil {
return nil, fmt.Errorf("failed to load config: %v", err)
}
return cfg, nil
}