Files
go-telegram-bot/config.go
HugeFrog24 166200c473 Security
2024-10-23 23:19:08 +02:00

189 lines
5.1 KiB
Go
Executable File

package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/liushuangls/go-anthropic/v2"
)
type BotConfig struct {
ID string `json:"id"`
TelegramToken string `json:"telegram_token"`
MemorySize int `json:"memory_size"`
MessagePerHour int `json:"messages_per_hour"`
MessagePerDay int `json:"messages_per_day"`
TempBanDuration string `json:"temp_ban_duration"`
Model anthropic.Model `json:"model"`
SystemPrompts map[string]string `json:"system_prompts"`
Active bool `json:"active"`
OwnerTelegramID int64 `json:"owner_telegram_id"`
AnthropicAPIKey string `json:"anthropic_api_key"`
}
// Custom unmarshalling to handle anthropic.Model
func (c *BotConfig) UnmarshalJSON(data []byte) error {
type Alias BotConfig
aux := &struct {
Model string `json:"model"`
*Alias
}{
Alias: (*Alias)(c),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
c.Model = anthropic.Model(aux.Model)
return nil
}
// validateConfigPath ensures the file path is within the allowed directory
func validateConfigPath(configDir, filename string) (string, error) {
// Clean the paths to remove any . or .. components
configDir = filepath.Clean(configDir)
filename = filepath.Clean(filename)
// Get absolute paths
absConfigDir, err := filepath.Abs(configDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for config directory: %w", err)
}
fullPath := filepath.Join(absConfigDir, filename)
absPath, err := filepath.Abs(fullPath)
if err != nil {
return "", fmt.Errorf("failed to get absolute path for config file: %w", err)
}
// Check if the file path is within the config directory
if !isSubPath(absConfigDir, absPath) {
return "", fmt.Errorf("invalid config path: file must be within the config directory")
}
// Verify file extension
if filepath.Ext(absPath) != ".json" {
return "", fmt.Errorf("invalid file extension: must be .json")
}
return absPath, nil
}
// isSubPath checks if childPath is a subdirectory of parentPath
func isSubPath(parentPath, childPath string) bool {
rel, err := filepath.Rel(parentPath, childPath)
if err != nil {
return false
}
return !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && rel != ".."
}
func loadAllConfigs(dir string) ([]BotConfig, error) {
var configs []BotConfig
ids := make(map[string]bool)
tokens := make(map[string]bool)
files, err := os.ReadDir(dir)
if err != nil {
return nil, fmt.Errorf("failed to read config directory: %w", err)
}
for _, file := range files {
if filepath.Ext(file.Name()) == ".json" {
validPath, err := validateConfigPath(dir, file.Name())
if err != nil {
return nil, fmt.Errorf("invalid config path: %w", err)
}
config, err := loadConfig(validPath)
if err != nil {
return nil, fmt.Errorf("failed to load config %s: %w", validPath, err)
}
// Validation checks...
if !config.Active {
InfoLogger.Printf("Skipping inactive bot: %s", config.ID)
continue
}
if err := validateConfig(&config, ids, tokens); err != nil {
return nil, fmt.Errorf("config validation failed for %s: %w", validPath, err)
}
configs = append(configs, config)
}
}
return configs, nil
}
func validateConfig(config *BotConfig, ids, tokens map[string]bool) error {
if config.ID == "" {
return fmt.Errorf("missing 'id' field")
}
if _, exists := ids[config.ID]; exists {
return fmt.Errorf("duplicate bot id '%s'", config.ID)
}
ids[config.ID] = true
if config.TelegramToken == "" {
return fmt.Errorf("missing 'telegram_token' field")
}
if _, exists := tokens[config.TelegramToken]; exists {
return fmt.Errorf("duplicate telegram_token")
}
tokens[config.TelegramToken] = true
if config.Model == "" {
return fmt.Errorf("missing 'model' field")
}
return nil
}
func loadConfig(filename string) (BotConfig, error) {
var config BotConfig
file, err := os.OpenFile(filename, os.O_RDONLY, 0)
if err != nil {
return config, fmt.Errorf("failed to open config file %s: %w", filename, err)
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(&config); err != nil {
return config, fmt.Errorf("failed to decode JSON from %s: %w", filename, err)
}
return config, nil
}
func (c *BotConfig) Reload(filename string) error {
// Get the directory of the current executable
execDir, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable directory: %w", err)
}
configDir := filepath.Dir(execDir)
// Validate the config path
validPath, err := validateConfigPath(configDir, filename)
if err != nil {
return fmt.Errorf("invalid config path: %w", err)
}
file, err := os.OpenFile(validPath, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("failed to open config file %s: %w", validPath, err)
}
defer file.Close()
decoder := json.NewDecoder(file)
if err := decoder.Decode(c); err != nil {
return fmt.Errorf("failed to decode JSON from %s: %w", validPath, err)
}
c.Model = anthropic.Model(c.Model)
return nil
}