302 lines
9.1 KiB
Go
302 lines
9.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"sync"
|
|
|
|
"github.com/pquerna/otp/totp"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type Filter struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
BlocklistFile string `json:"blocklist_file"` // Just filename, path constructed from domain_list_folder
|
|
WhitelistFile string `json:"whitelist_file"` // Just filename, path constructed from domain_list_folder
|
|
BlocklistURLs []URLListItem `json:"blocklist_urls"`
|
|
WhitelistURLs []URLListItem `json:"whitelist_urls"`
|
|
}
|
|
|
|
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 - folders
|
|
PidFile string `json:"pid_file"`
|
|
ProcessName string `json:"process_name"`
|
|
DomainListFolder string `json:"domain_list_folder"` // Base folder for blocklist/whitelist files
|
|
TmpFolder string `json:"tmp_folder"` // Folder for temporary blocklist files
|
|
LastUpdateFolder string `json:"last_update_folder"` // Folder for last update timestamp files
|
|
MergedListTmpFolder string `json:"merged_list_tmp_folder"` // Folder for merged list temporary files
|
|
|
|
// Filters
|
|
Filters []Filter `json:"filters"`
|
|
}
|
|
|
|
var (
|
|
config Config
|
|
configFile = "config.json"
|
|
configMu sync.RWMutex
|
|
)
|
|
|
|
func getDefaultConfig() Config {
|
|
blocklistURLs := []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"},
|
|
}
|
|
|
|
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",
|
|
DomainListFolder: "/run/utm/domain_list/",
|
|
TmpFolder: "/tmp/",
|
|
LastUpdateFolder: "/sdcard1/",
|
|
MergedListTmpFolder: "/tmp/",
|
|
Filters: []Filter{
|
|
{
|
|
Name: "Default",
|
|
Description: "Default blocklist filter",
|
|
BlocklistFile: "domainlist_0.list",
|
|
WhitelistFile: "domainlist_1.list",
|
|
BlocklistURLs: blocklistURLs,
|
|
WhitelistURLs: []URLListItem{},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
func getFilter(filterName string) *Filter {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
|
|
for i := range config.Filters {
|
|
if config.Filters[i].Name == filterName {
|
|
return &config.Filters[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateFilterBlocklistURLs(filterName string, urls []URLListItem) error {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
|
|
for i := range config.Filters {
|
|
if config.Filters[i].Name == filterName {
|
|
config.Filters[i].BlocklistURLs = urls
|
|
saveConfigLocked()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("filter not found")
|
|
}
|
|
|
|
func updateFilterWhitelistURLs(filterName string, urls []URLListItem) error {
|
|
configMu.Lock()
|
|
defer configMu.Unlock()
|
|
|
|
for i := range config.Filters {
|
|
if config.Filters[i].Name == filterName {
|
|
config.Filters[i].WhitelistURLs = urls
|
|
saveConfigLocked()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("filter not found")
|
|
}
|
|
|
|
func getFilterBlocklistPath(filterName string) string {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
|
|
for i := range config.Filters {
|
|
if config.Filters[i].Name == filterName {
|
|
return config.DomainListFolder + config.Filters[i].BlocklistFile
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFilterWhitelistPath(filterName string) string {
|
|
configMu.RLock()
|
|
defer configMu.RUnlock()
|
|
|
|
for i := range config.Filters {
|
|
if config.Filters[i].Name == filterName {
|
|
return config.DomainListFolder + config.Filters[i].WhitelistFile
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func getFilterTmpPath(filterName string) string {
|
|
configMu.RLock()
|
|
tmpFolder := config.TmpFolder
|
|
configMu.RUnlock()
|
|
|
|
return tmpFolder + "blocklist_" + filterName + ".tmp"
|
|
}
|
|
|
|
func getFilterLastUpdatePath(filterName string) string {
|
|
configMu.RLock()
|
|
lastUpdateFolder := config.LastUpdateFolder
|
|
configMu.RUnlock()
|
|
|
|
return lastUpdateFolder + "blocklist_" + filterName + "_lastupdate.txt"
|
|
}
|
|
|
|
func getFilterMergedListTmpPath(filterName string) string {
|
|
configMu.RLock()
|
|
mergedFolder := config.MergedListTmpFolder
|
|
configMu.RUnlock()
|
|
|
|
return mergedFolder + "blocklist_" + filterName + "_merged.tmp"
|
|
}
|
|
|