package main import ( "encoding/json" "fmt" "log" "os" "sync" "github.com/pquerna/otp/totp" "golang.org/x/crypto/bcrypt" ) type Config struct { // Application AppName string `json:"app_name"` // Authentication Username string `json:"username"` HashedPass string `json:"hashed_password"` MFASecret string `json:"mfa_secret"` SessionTimeoutMins int `json:"session_timeout_minutes"` // Server Port string `json:"port"` BindAddr string `json:"bind_address"` // Blocklist settings UpdateDelay int `json:"update_delay_days"` CheckInterval int `json:"check_interval_seconds"` // File paths PidFile string `json:"pid_file"` ProcessName string `json:"process_name"` TmpFile string `json:"tmp_file"` LastUpdateFile string `json:"last_update_file"` URLFileList string `json:"url_file_list"` BlocklistFile string `json:"blocklist_file"` RemoveFile string `json:"remove_file"` WhitelistFile string `json:"whitelist_file"` MergedListTmp string `json:"merged_list_tmp"` // Default blocklist URLs DefaultURLs []URLListItem `json:"default_urls"` } var ( config Config configFile = "config.json" configMu sync.RWMutex ) func getDefaultConfig() Config { return Config{ AppName: "Unifi Blocklist Manager", Username: "admin", HashedPass: "", // Will be set with hashed password MFASecret: "", SessionTimeoutMins: 60, // 1 hour default Port: "8080", BindAddr: "0.0.0.0", UpdateDelay: 3, CheckInterval: 5, PidFile: "/tmp/coredns_last.pid", ProcessName: "coredns", TmpFile: "/sdcard1/combined-blocklist.txt", LastUpdateFile: "/sdcard1/last_update.txt", URLFileList: "/sdcard1/urllist.csv", BlocklistFile: "/run/utm/domain_list/domainlist_0.list", RemoveFile: "/run/utm/domain_list/domainlist_1.list", WhitelistFile: "custom_whitelist.txt", MergedListTmp: "/tmp/mergedlist.txt", DefaultURLs: []URLListItem{ {Name: "AdGuard DNS filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_27.txt"}, {Name: "AdGuard Russian filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_49.txt"}, {Name: "AdGuard Base filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt"}, {Name: "AdGuard Tracking Protection", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_42.txt"}, {Name: "AdGuard Social Media filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_18.txt"}, {Name: "AdGuard Annoyance filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_23.txt"}, {Name: "AdGuard Mobile Ads filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_11.txt"}, {Name: "AdGuard Search Ads filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_9.txt"}, {Name: "AdGuard Adult filter", Enabled: true, Group: "Default", URL: "https://adguardteam.github.io/HostlistsRegistry/assets/filter_50.txt"}, {Name: "AntiMalware Hosts", Enabled: true, Group: "Default", URL: "https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareHosts.txt"}, {Name: "DigitalSide Threat Intel", Enabled: true, Group: "Default", URL: "https://osint.digitalside.it/Threat-Intel/lists/latestdomains.txt"}, {Name: "Firebog Crypto", Enabled: true, Group: "Default", URL: "https://v.firebog.net/hosts/Prigent-Crypto.txt"}, {Name: "Phishing Army", Enabled: true, Group: "Default", URL: "https://phishing.army/download/phishing_army_blocklist_extended.txt"}, {Name: "W3kbl", Enabled: true, Group: "Default", URL: "https://v.firebog.net/hosts/static/w3kbl.txt"}, }, } } func loadConfig() { configMu.Lock() defer configMu.Unlock() file, err := os.Open(configFile) if err != nil { log.Println("Config not found, creating default") // Create default config with admin user defaultConfig := getDefaultConfig() hashed, _ := bcrypt.GenerateFromPassword([]byte("Password123!"), bcrypt.DefaultCost) defaultConfig.HashedPass = string(hashed) config = defaultConfig saveConfigLocked() log.Println("Default config created with admin/Password123!") return } defer file.Close() err = json.NewDecoder(file).Decode(&config) if err != nil { log.Printf("Error reading config: %v, using defaults", err) defaultConfig := getDefaultConfig() hashed, _ := bcrypt.GenerateFromPassword([]byte("Password123!"), bcrypt.DefaultCost) defaultConfig.HashedPass = string(hashed) config = defaultConfig } } func saveConfigLocked() { file, err := os.Create(configFile) if err != nil { log.Fatal(err) } defer file.Close() encoder := json.NewEncoder(file) encoder.SetIndent("", " ") err = encoder.Encode(config) if err != nil { log.Fatal(err) } } func saveConfig() { configMu.Lock() defer configMu.Unlock() saveConfigLocked() } func handleMFACommand(cmd string) { switch cmd { case "on": configMu.Lock() if config.MFASecret == "" { // Generate new secret key, err := totp.Generate(totp.GenerateOpts{ Issuer: "UnifiBlocklist", AccountName: config.Username, }) if err != nil { fmt.Fprintf(os.Stderr, "Error generating MFA: %v\n", err) configMu.Unlock() os.Exit(1) } config.MFASecret = key.Secret() saveConfigLocked() fmt.Printf("MFA enabled.\nSecret: %s\nOTP URL: %s\n", key.Secret(), key.URL()) } else { fmt.Println("MFA is already enabled") configMu.Unlock() return } configMu.Unlock() case "off": configMu.Lock() if config.MFASecret != "" { config.MFASecret = "" saveConfigLocked() fmt.Println("MFA disabled") } else { fmt.Println("MFA is not enabled") } configMu.Unlock() case "reset": configMu.Lock() key, err := totp.Generate(totp.GenerateOpts{ Issuer: "UnifiBlocklist", AccountName: config.Username, }) if err != nil { fmt.Fprintf(os.Stderr, "Error generating MFA: %v\n", err) configMu.Unlock() os.Exit(1) } config.MFASecret = key.Secret() saveConfigLocked() fmt.Printf("MFA reset.\nNew Secret: %s\nOTP URL: %s\nScan with your authenticator app.\n", key.Secret(), key.URL()) configMu.Unlock() default: fmt.Fprintf(os.Stderr, "Invalid MFA command. Use: on, off, or reset\n") os.Exit(1) } }