1018 lines
27 KiB
Go
1018 lines
27 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const sessionName = "session"
|
|
|
|
func profileHandler(w http.ResponseWriter, r *http.Request) {
|
|
configMu.RLock()
|
|
mfaEnabled := config.MFASecret != ""
|
|
configMu.RUnlock()
|
|
|
|
session, _ := store.Get(r, sessionName)
|
|
|
|
data := TemplateData{
|
|
MFAEnabled: mfaEnabled,
|
|
}
|
|
|
|
// Check if MFA setup is in progress
|
|
if setupSecret, ok := session.Values["mfa_setup_secret"].(string); ok && setupSecret != "" {
|
|
if setupURL, ok := session.Values["mfa_setup_url"].(string); ok {
|
|
data.MFASetupInProgress = true
|
|
data.MFASecret = setupSecret
|
|
data.MFAURL = setupURL
|
|
}
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
action := r.FormValue("action")
|
|
switch action {
|
|
case "change_password":
|
|
current := r.FormValue("current_password")
|
|
newpw := r.FormValue("new_password")
|
|
confirmpw := r.FormValue("confirm_new_password")
|
|
mfaCode := r.FormValue("mfa_code")
|
|
|
|
// Validate current password
|
|
if bcrypt.CompareHashAndPassword([]byte(config.HashedPass), []byte(current)) != nil {
|
|
data.Notification = "Current password is incorrect."
|
|
data.NotificationType = "error"
|
|
renderTemplate(w, r, "profile.html", data)
|
|
return
|
|
}
|
|
// Check if new passwords match
|
|
if newpw != confirmpw {
|
|
data.Notification = "New passwords do not match."
|
|
data.NotificationType = "error"
|
|
renderTemplate(w, r, "profile.html", data)
|
|
return
|
|
}
|
|
// If MFA enabled, validate code
|
|
if config.MFASecret != "" {
|
|
if !validateTOTP(config.MFASecret, mfaCode) {
|
|
data.Notification = "Invalid MFA code."
|
|
data.NotificationType = "error"
|
|
renderTemplate(w, r, "profile.html", data)
|
|
return
|
|
}
|
|
}
|
|
// Validate new password
|
|
if len(newpw) < 8 {
|
|
data.Notification = "Password must be at least 8 characters."
|
|
data.NotificationType = "error"
|
|
renderTemplate(w, r, "profile.html", data)
|
|
return
|
|
}
|
|
hash, _ := bcrypt.GenerateFromPassword([]byte(newpw), bcrypt.DefaultCost)
|
|
config.HashedPass = string(hash)
|
|
saveConfig()
|
|
// Invalidate session
|
|
session.Options.MaxAge = -1
|
|
session.Save(r, w)
|
|
data.Notification = "Password changed successfully. Please log in again."
|
|
data.NotificationType = "success"
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
return
|
|
|
|
case "toggle_mfa":
|
|
mfaAction := r.FormValue("mfa")
|
|
if mfaAction == "on" && config.MFASecret == "" {
|
|
// Generate new MFA secret and store in session
|
|
secret, qrURL := generateMFASecret(config.Username, config.AppName)
|
|
session.Values["mfa_setup_secret"] = secret
|
|
session.Values["mfa_setup_url"] = qrURL
|
|
session.Save(r, w)
|
|
data.MFASetupInProgress = true
|
|
data.MFASecret = secret
|
|
data.MFAURL = qrURL
|
|
data.Notification = "MFA setup initiated. Please configure your authenticator app."
|
|
data.NotificationType = "info"
|
|
} else if mfaAction == "off" && config.MFASecret != "" {
|
|
config.MFASecret = ""
|
|
saveConfig()
|
|
data.Notification = "MFA disabled."
|
|
data.NotificationType = "success"
|
|
data.MFAEnabled = false
|
|
}
|
|
|
|
case "confirm_mfa":
|
|
mfaCode := r.FormValue("mfa_code")
|
|
if setupSecret, ok := session.Values["mfa_setup_secret"].(string); ok && setupSecret != "" {
|
|
if validateTOTP(setupSecret, mfaCode) {
|
|
config.MFASecret = setupSecret
|
|
saveConfig()
|
|
// Clear session
|
|
delete(session.Values, "mfa_setup_secret")
|
|
delete(session.Values, "mfa_setup_url")
|
|
session.Save(r, w)
|
|
data.Notification = "MFA enabled successfully."
|
|
data.NotificationType = "success"
|
|
data.MFAEnabled = true
|
|
data.MFASetupInProgress = false
|
|
data.MFASecret = ""
|
|
data.MFAURL = ""
|
|
} else {
|
|
data.Notification = "Invalid MFA code. Please try again."
|
|
data.NotificationType = "error"
|
|
}
|
|
}
|
|
|
|
case "cancel_mfa":
|
|
// Clear session
|
|
delete(session.Values, "mfa_setup_secret")
|
|
delete(session.Values, "mfa_setup_url")
|
|
session.Save(r, w)
|
|
data.MFASetupInProgress = false
|
|
data.MFASecret = ""
|
|
data.MFAURL = ""
|
|
data.Notification = "MFA setup cancelled."
|
|
data.NotificationType = "info"
|
|
}
|
|
}
|
|
|
|
renderTemplate(w, r, "profile.html", data)
|
|
}
|
|
|
|
func mfaSetupHandler(w http.ResponseWriter, r *http.Request) {
|
|
if config.MFASecret != "" {
|
|
http.Error(w, "MFA already enabled", 400)
|
|
return
|
|
}
|
|
secret, qrURL := generateMFASecret(config.Username, config.AppName)
|
|
// Store in session
|
|
session, _ := store.Get(r, sessionName)
|
|
session.Values["mfa_setup_secret"] = secret
|
|
session.Values["mfa_setup_url"] = qrURL
|
|
session.Save(r, w)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"secret": secret, "url": qrURL})
|
|
}
|
|
|
|
func dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
|
// Redirect to filters page
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
}
|
|
|
|
func urlListsHandler(w http.ResponseWriter, r *http.Request) {
|
|
configMu.RLock()
|
|
var blocklistURLs []URLListItem
|
|
if len(config.Filters) > 0 {
|
|
blocklistURLs = config.Filters[0].BlocklistURLs
|
|
}
|
|
configMu.RUnlock()
|
|
|
|
urlFileList := "blocklist_urls.csv"
|
|
|
|
if r.Method == "POST" {
|
|
action := r.FormValue("action")
|
|
|
|
items, err := readURLListCSV(urlFileList)
|
|
if err != nil {
|
|
renderTemplate(w, r, "urllists.html", struct {
|
|
Items []URLListItem
|
|
Notification string
|
|
NotificationType string
|
|
}{
|
|
Items: items,
|
|
Notification: "Error reading URL list",
|
|
NotificationType: "error",
|
|
})
|
|
return
|
|
}
|
|
|
|
var errorMsg string
|
|
|
|
switch action {
|
|
case "add":
|
|
name := sanitizeInput(r.FormValue("name"))
|
|
urlStr := sanitizeInput(r.FormValue("url"))
|
|
group := sanitizeInput(r.FormValue("group"))
|
|
note := sanitizeInput(r.FormValue("note"))
|
|
|
|
if name == "" {
|
|
errorMsg = "Name is required"
|
|
} else if !isValidURL(urlStr) {
|
|
errorMsg = "Invalid URL format. Must start with http:// or https://"
|
|
} else {
|
|
items = append(items, URLListItem{
|
|
Name: name,
|
|
Enabled: true,
|
|
Group: group,
|
|
URL: urlStr,
|
|
Note: note,
|
|
})
|
|
}
|
|
|
|
case "remove":
|
|
indexStr := r.FormValue("index")
|
|
index, _ := strconv.Atoi(indexStr)
|
|
if index >= 0 && index < len(items) {
|
|
items = append(items[:index], items[index+1:]...)
|
|
} else {
|
|
errorMsg = "Invalid index"
|
|
}
|
|
|
|
case "toggle":
|
|
indexStr := r.FormValue("index")
|
|
index, _ := strconv.Atoi(indexStr)
|
|
if index >= 0 && index < len(items) {
|
|
items[index].Enabled = !items[index].Enabled
|
|
} else {
|
|
errorMsg = "Invalid index"
|
|
}
|
|
|
|
case "edit":
|
|
indexStr := r.FormValue("index")
|
|
name := sanitizeInput(r.FormValue("name"))
|
|
urlStr := sanitizeInput(r.FormValue("url"))
|
|
group := sanitizeInput(r.FormValue("group"))
|
|
note := sanitizeInput(r.FormValue("note"))
|
|
|
|
index, _ := strconv.Atoi(indexStr)
|
|
if index >= 0 && index < len(items) {
|
|
if name == "" {
|
|
errorMsg = "Name is required"
|
|
} else if !isValidURL(urlStr) {
|
|
errorMsg = "Invalid URL format. Must start with http:// or https://"
|
|
} else {
|
|
items[index].Name = name
|
|
items[index].URL = urlStr
|
|
items[index].Group = group
|
|
items[index].Note = note
|
|
}
|
|
} else {
|
|
errorMsg = "Invalid index"
|
|
}
|
|
}
|
|
|
|
if errorMsg != "" {
|
|
renderTemplate(w, r, "urllists.html", struct {
|
|
Items []URLListItem
|
|
Notification string
|
|
NotificationType string
|
|
}{
|
|
Items: items,
|
|
Notification: errorMsg,
|
|
NotificationType: "error",
|
|
})
|
|
return
|
|
}
|
|
|
|
err = writeURLListCSV(urlFileList, items)
|
|
if err != nil {
|
|
renderTemplate(w, r, "urllists.html", struct {
|
|
Items []URLListItem
|
|
Notification string
|
|
NotificationType string
|
|
}{
|
|
Items: items,
|
|
Notification: "Error saving URL list",
|
|
NotificationType: "error",
|
|
})
|
|
return
|
|
}
|
|
|
|
renderTemplate(w, r, "urllists.html", struct {
|
|
Items []URLListItem
|
|
Notification string
|
|
NotificationType string
|
|
}{
|
|
Items: items,
|
|
Notification: "URL list updated successfully",
|
|
NotificationType: "success",
|
|
})
|
|
return
|
|
}
|
|
|
|
items, err := readURLListCSV(urlFileList)
|
|
if err != nil || len(items) == 0 {
|
|
// Use default blocklists
|
|
defaultItems := blocklistURLs
|
|
writeURLListCSV(urlFileList, defaultItems)
|
|
items = defaultItems
|
|
}
|
|
|
|
renderTemplate(w, r, "urllists.html", struct {
|
|
Items []URLListItem
|
|
}{
|
|
Items: items,
|
|
})
|
|
}
|
|
|
|
func domainsHandler(w http.ResponseWriter, r *http.Request) {
|
|
pageData := map[string]interface{}{
|
|
"Domains": []string{},
|
|
"Query": "",
|
|
"Notification": "",
|
|
"NotificationType": "",
|
|
}
|
|
|
|
renderTemplate(w, r, "domains.html", pageData)
|
|
}
|
|
|
|
func whitelistHandler(w http.ResponseWriter, r *http.Request) {
|
|
pageData := map[string]interface{}{
|
|
"Domains": []string{},
|
|
"Query": "",
|
|
"Notification": "",
|
|
"NotificationType": "",
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
action := r.FormValue("action")
|
|
if action == "add_domain" {
|
|
domain := strings.TrimSpace(r.FormValue("domain"))
|
|
if domain == "" {
|
|
pageData["Notification"] = "Domain cannot be empty."
|
|
pageData["NotificationType"] = "error"
|
|
} else {
|
|
configMu.RLock()
|
|
whitelistFile := ""
|
|
domainListFolder := config.DomainListFolder
|
|
if len(config.Filters) > 0 {
|
|
whitelistFile = domainListFolder + config.Filters[0].WhitelistFile
|
|
}
|
|
configMu.RUnlock()
|
|
|
|
// Read current whitelist
|
|
lines, err := readLines(whitelistFile)
|
|
if err != nil {
|
|
lines = []string{}
|
|
}
|
|
|
|
// Check if already exists
|
|
exists := false
|
|
for _, d := range lines {
|
|
if strings.EqualFold(d, domain) {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if exists {
|
|
pageData["Notification"] = "Domain already in whitelist."
|
|
pageData["NotificationType"] = "error"
|
|
} else {
|
|
lines = append(lines, domain)
|
|
writeLines(whitelistFile, lines)
|
|
pageData["Notification"] = "Domain added to whitelist."
|
|
pageData["NotificationType"] = "success"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
renderTemplate(w, r, "whitelist.html", pageData)
|
|
}
|
|
|
|
func searchDomainsHandler(w http.ResponseWriter, r *http.Request) {
|
|
configMu.RLock()
|
|
blocklistFile := ""
|
|
domainListFolder := config.DomainListFolder
|
|
if len(config.Filters) > 0 {
|
|
blocklistFile = domainListFolder + config.Filters[0].BlocklistFile
|
|
}
|
|
configMu.RUnlock()
|
|
|
|
query := strings.TrimSpace(r.FormValue("query"))
|
|
if query == "" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"domains": []string{}})
|
|
return
|
|
}
|
|
|
|
domains, err := readLines(blocklistFile)
|
|
if err != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"domains": []string{}})
|
|
return
|
|
}
|
|
|
|
var filtered []string
|
|
if strings.Contains(query, "*") {
|
|
// Wildcard search
|
|
pattern := strings.ReplaceAll(regexp.QuoteMeta(query), "\\*", ".*")
|
|
re, err := regexp.Compile("(?i)^" + pattern + "$")
|
|
if err == nil {
|
|
for _, d := range domains {
|
|
if re.MatchString(d) {
|
|
filtered = append(filtered, d)
|
|
if len(filtered) >= 100 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Exact match, case insensitive
|
|
for _, d := range domains {
|
|
if strings.EqualFold(d, query) {
|
|
filtered = append(filtered, d)
|
|
if len(filtered) >= 100 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"domains": filtered})
|
|
}
|
|
|
|
func searchWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
|
configMu.RLock()
|
|
domainListFolder := config.DomainListFolder
|
|
removeFile := ""
|
|
whitelistFile := ""
|
|
if len(config.Filters) > 0 {
|
|
removeFile = domainListFolder + config.Filters[0].BlocklistFile
|
|
whitelistFile = domainListFolder + config.Filters[0].WhitelistFile
|
|
}
|
|
configMu.RUnlock()
|
|
|
|
query := strings.TrimSpace(r.FormValue("query"))
|
|
if query == "" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": []map[string]string{}})
|
|
return
|
|
}
|
|
|
|
// Read both files
|
|
domains1, err1 := readLines(removeFile)
|
|
domains2, err2 := readLines(whitelistFile)
|
|
if err1 != nil && err2 != nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": []map[string]string{}})
|
|
return
|
|
}
|
|
|
|
// Create maps for quick lookup
|
|
removeMap := make(map[string]bool)
|
|
for _, d := range domains1 {
|
|
removeMap[strings.ToLower(d)] = true
|
|
}
|
|
whitelistMap := make(map[string]bool)
|
|
for _, d := range domains2 {
|
|
whitelistMap[strings.ToLower(d)] = true
|
|
}
|
|
|
|
// Combine unique domains
|
|
domainMap := make(map[string]bool)
|
|
for d := range removeMap {
|
|
domainMap[d] = true
|
|
}
|
|
for d := range whitelistMap {
|
|
domainMap[d] = true
|
|
}
|
|
|
|
var filtered []map[string]string
|
|
for d := range domainMap {
|
|
var source string
|
|
inRemove := removeMap[d]
|
|
inWhitelist := whitelistMap[d]
|
|
if inRemove && inWhitelist {
|
|
source = "Both"
|
|
} else if inRemove {
|
|
source = "Unifi"
|
|
} else {
|
|
source = "Custom"
|
|
}
|
|
|
|
match := false
|
|
if strings.Contains(query, "*") {
|
|
// Wildcard search
|
|
pattern := strings.ReplaceAll(regexp.QuoteMeta(query), "\\*", ".*")
|
|
re, err := regexp.Compile("(?i)^" + pattern + "$")
|
|
if err == nil && re.MatchString(d) {
|
|
match = true
|
|
}
|
|
} else {
|
|
// Exact match, case insensitive
|
|
if strings.EqualFold(d, query) {
|
|
match = true
|
|
}
|
|
}
|
|
|
|
if match {
|
|
filtered = append(filtered, map[string]string{"domain": d, "source": source})
|
|
if len(filtered) >= 100 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": filtered})
|
|
}
|
|
|
|
func applyHandler(w http.ResponseWriter, r *http.Request) {
|
|
userInitiatedMu.Lock()
|
|
userInitiated = true
|
|
userInitiatedMu.Unlock()
|
|
|
|
updateBlocklist(1) // Force
|
|
|
|
userInitiatedMu.Lock()
|
|
userInitiated = false
|
|
userInitiatedMu.Unlock()
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
}
|
|
|
|
func logsHandler(w http.ResponseWriter, r *http.Request) {
|
|
// For simplicity, assume logs are in stdout, or implement file logging
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("Logs placeholder"))
|
|
}
|
|
|
|
func filtersHandler(w http.ResponseWriter, r *http.Request) {
|
|
configMu.RLock()
|
|
filters := config.Filters
|
|
configMu.RUnlock()
|
|
|
|
pageData := map[string]interface{}{
|
|
"Filters": filters,
|
|
}
|
|
|
|
renderTemplate(w, r, "filters.html", pageData)
|
|
}
|
|
|
|
func createFilterHandler(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Verify CSRF token
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
description := strings.TrimSpace(r.FormValue("description"))
|
|
blocklistFile := strings.TrimSpace(r.FormValue("blocklist_file"))
|
|
whitelistFile := strings.TrimSpace(r.FormValue("whitelist_file"))
|
|
|
|
// Validation
|
|
if name == "" || blocklistFile == "" || whitelistFile == "" {
|
|
pageData := map[string]interface{}{
|
|
"Notification": "All fields are required",
|
|
"NotificationType": "error",
|
|
}
|
|
renderTemplate(w, r, "filters.html", pageData)
|
|
return
|
|
}
|
|
|
|
// Check if filter already exists and add to config
|
|
configMu.Lock()
|
|
|
|
for _, f := range config.Filters {
|
|
if f.Name == name {
|
|
configMu.Unlock()
|
|
pageData := map[string]interface{}{
|
|
"Notification": "Filter with this name already exists",
|
|
"NotificationType": "error",
|
|
}
|
|
renderTemplate(w, r, "filters.html", pageData)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Create new filter
|
|
newFilter := Filter{
|
|
Name: name,
|
|
Description: description,
|
|
BlocklistFile: blocklistFile,
|
|
WhitelistFile: whitelistFile,
|
|
}
|
|
|
|
// Add to config and save
|
|
config.Filters = append(config.Filters, newFilter)
|
|
saveConfigLocked()
|
|
|
|
configMu.Unlock()
|
|
|
|
// Redirect back to filters with success message
|
|
http.Redirect(w, r, "/filters?success=1", http.StatusSeeOther)
|
|
}
|
|
|
|
func editFilterHandler(w http.ResponseWriter, r *http.Request) {
|
|
filterName := r.URL.Query().Get("filter")
|
|
if filterName == "" {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
filter := getFilter(filterName)
|
|
if filter == nil {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
if r.Method == "POST" {
|
|
// Handle edit form submission
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
newName := strings.TrimSpace(r.FormValue("name"))
|
|
description := strings.TrimSpace(r.FormValue("description"))
|
|
blocklistFile := strings.TrimSpace(r.FormValue("blocklist_file"))
|
|
whitelistFile := strings.TrimSpace(r.FormValue("whitelist_file"))
|
|
|
|
// Validation
|
|
if newName == "" || blocklistFile == "" || whitelistFile == "" {
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
"Notification": "All fields are required",
|
|
"NotificationType": "error",
|
|
}
|
|
renderTemplate(w, r, "edit-filter.html", pageData)
|
|
return
|
|
}
|
|
|
|
// Check if new name conflicts with existing filter and update it
|
|
configMu.Lock()
|
|
|
|
// Check for name conflict
|
|
nameConflict := false
|
|
for _, f := range config.Filters {
|
|
if f.Name == newName && f.Name != filterName {
|
|
nameConflict = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if nameConflict {
|
|
configMu.Unlock()
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
"Notification": "Filter with this name already exists",
|
|
"NotificationType": "error",
|
|
}
|
|
renderTemplate(w, r, "edit-filter.html", pageData)
|
|
return
|
|
}
|
|
|
|
// Update filter
|
|
for i, f := range config.Filters {
|
|
if f.Name == filterName {
|
|
config.Filters[i].Name = newName
|
|
config.Filters[i].Description = description
|
|
config.Filters[i].BlocklistFile = blocklistFile
|
|
config.Filters[i].WhitelistFile = whitelistFile
|
|
break
|
|
}
|
|
}
|
|
|
|
// Save config using saveConfigLocked since we already hold the lock
|
|
saveConfigLocked()
|
|
|
|
configMu.Unlock()
|
|
|
|
// Redirect back to filters (after unlock)
|
|
http.Redirect(w, r, "/filters?success=1", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Display edit form (GET request)
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
}
|
|
|
|
renderTemplate(w, r, "edit-filter.html", pageData)
|
|
}
|
|
|
|
func filterDetailHandler(w http.ResponseWriter, r *http.Request) {
|
|
filterName := r.URL.Query().Get("filter")
|
|
if filterName == "" {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
filter := getFilter(filterName)
|
|
if filter == nil {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
"Notification": "",
|
|
"NotificationType": "",
|
|
}
|
|
|
|
renderTemplate(w, r, "filter-detail.html", pageData)
|
|
}
|
|
|
|
func filterSearchHandler(w http.ResponseWriter, r *http.Request) {
|
|
filterName := strings.TrimSpace(r.FormValue("filter"))
|
|
query := strings.TrimSpace(r.FormValue("query"))
|
|
|
|
if filterName == "" || query == "" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": []map[string]string{}})
|
|
return
|
|
}
|
|
|
|
filter := getFilter(filterName)
|
|
if filter == nil {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": []map[string]string{}})
|
|
return
|
|
}
|
|
|
|
configMu.RLock()
|
|
domainListFolder := config.DomainListFolder
|
|
configMu.RUnlock()
|
|
|
|
blocklistPath := domainListFolder + filter.BlocklistFile
|
|
whitelistPath := domainListFolder + filter.WhitelistFile
|
|
|
|
// Read both files
|
|
blocklist, _ := readLines(blocklistPath)
|
|
whitelist, _ := readLines(whitelistPath)
|
|
|
|
// Create maps for quick lookup
|
|
blocklistMap := make(map[string]bool)
|
|
for _, d := range blocklist {
|
|
blocklistMap[strings.ToLower(d)] = true
|
|
}
|
|
whitelistMap := make(map[string]bool)
|
|
for _, d := range whitelist {
|
|
whitelistMap[strings.ToLower(d)] = true
|
|
}
|
|
|
|
// Combine unique domains
|
|
domainMap := make(map[string]bool)
|
|
for d := range blocklistMap {
|
|
domainMap[d] = true
|
|
}
|
|
for d := range whitelistMap {
|
|
domainMap[d] = true
|
|
}
|
|
|
|
var filtered []map[string]string
|
|
for d := range domainMap {
|
|
var source string
|
|
inBlocklist := blocklistMap[d]
|
|
inWhitelist := whitelistMap[d]
|
|
if inBlocklist && inWhitelist {
|
|
source = "Both"
|
|
} else if inBlocklist {
|
|
source = "Blocklist"
|
|
} else {
|
|
source = "Whitelist"
|
|
}
|
|
|
|
match := false
|
|
if strings.Contains(query, "*") {
|
|
// Wildcard search
|
|
pattern := strings.ReplaceAll(regexp.QuoteMeta(query), "\\*", ".*")
|
|
re, err := regexp.Compile("(?i)^" + pattern + "$")
|
|
if err == nil && re.MatchString(d) {
|
|
match = true
|
|
}
|
|
} else {
|
|
// Exact match, case insensitive
|
|
if strings.EqualFold(d, query) {
|
|
match = true
|
|
}
|
|
}
|
|
|
|
if match {
|
|
filtered = append(filtered, map[string]string{"domain": d, "source": source})
|
|
if len(filtered) >= 100 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]interface{}{"results": filtered})
|
|
}
|
|
|
|
func filterBlocklistHandler(w http.ResponseWriter, r *http.Request) {
|
|
filterName := r.URL.Query().Get("filter")
|
|
if filterName == "" {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
filter := getFilter(filterName)
|
|
if filter == nil {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Read current blocklist domains
|
|
configMu.RLock()
|
|
domainListFolder := config.DomainListFolder
|
|
configMu.RUnlock()
|
|
|
|
blocklistPath := domainListFolder + filter.BlocklistFile
|
|
domains, _ := readLines(blocklistPath)
|
|
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
"FilterName": filterName,
|
|
"URLs": filter.BlocklistURLs,
|
|
"Domains": domains,
|
|
"Notification": "",
|
|
"NotificationType": "",
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
action := r.FormValue("action")
|
|
|
|
switch action {
|
|
case "add_domain":
|
|
domain := strings.TrimSpace(r.FormValue("domain"))
|
|
if domain == "" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Domain is required"})
|
|
return
|
|
}
|
|
// Add domain to file
|
|
domains = append(domains, domain)
|
|
writeLines(blocklistPath, domains)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
return
|
|
|
|
case "remove_domain":
|
|
domain := strings.TrimSpace(r.FormValue("domain"))
|
|
// Remove domain from slice
|
|
for i, d := range domains {
|
|
if d == domain {
|
|
domains = append(domains[:i], domains[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
writeLines(blocklistPath, domains)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
return
|
|
|
|
case "add_url":
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
url := strings.TrimSpace(r.FormValue("url"))
|
|
if name == "" || url == "" {
|
|
pageData["Notification"] = "Name and URL are required."
|
|
pageData["NotificationType"] = "error"
|
|
} else {
|
|
newURL := URLListItem{Name: name, URL: url, Enabled: true, Group: "Custom"}
|
|
filter.BlocklistURLs = append(filter.BlocklistURLs, newURL)
|
|
updateFilterBlocklistURLs(filterName, filter.BlocklistURLs)
|
|
pageData["Notification"] = "URL added successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.BlocklistURLs
|
|
}
|
|
case "toggle_url":
|
|
idx, _ := strconv.Atoi(r.FormValue("index"))
|
|
if idx >= 0 && idx < len(filter.BlocklistURLs) {
|
|
filter.BlocklistURLs[idx].Enabled = !filter.BlocklistURLs[idx].Enabled
|
|
updateFilterBlocklistURLs(filterName, filter.BlocklistURLs)
|
|
pageData["Notification"] = "URL toggled successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.BlocklistURLs
|
|
}
|
|
case "remove_url":
|
|
idx, _ := strconv.Atoi(r.FormValue("index"))
|
|
if idx >= 0 && idx < len(filter.BlocklistURLs) {
|
|
filter.BlocklistURLs = append(filter.BlocklistURLs[:idx], filter.BlocklistURLs[idx+1:]...)
|
|
updateFilterBlocklistURLs(filterName, filter.BlocklistURLs)
|
|
pageData["Notification"] = "URL removed successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.BlocklistURLs
|
|
}
|
|
}
|
|
}
|
|
|
|
renderTemplate(w, r, "filter-blocklist.html", pageData)
|
|
}
|
|
|
|
func filterWhitelistHandler(w http.ResponseWriter, r *http.Request) {
|
|
filterName := r.URL.Query().Get("filter")
|
|
if filterName == "" {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
filter := getFilter(filterName)
|
|
if filter == nil {
|
|
http.Redirect(w, r, "/filters", http.StatusSeeOther)
|
|
return
|
|
}
|
|
|
|
// Read current whitelist domains
|
|
configMu.RLock()
|
|
domainListFolder := config.DomainListFolder
|
|
configMu.RUnlock()
|
|
|
|
whitelistPath := domainListFolder + filter.WhitelistFile
|
|
domains, _ := readLines(whitelistPath)
|
|
|
|
pageData := map[string]interface{}{
|
|
"Filter": filter,
|
|
"FilterName": filterName,
|
|
"URLs": filter.WhitelistURLs,
|
|
"Domains": domains,
|
|
"Notification": "",
|
|
"NotificationType": "",
|
|
}
|
|
|
|
if r.Method == http.MethodPost {
|
|
if err := r.ParseForm(); err != nil {
|
|
http.Error(w, "Invalid form", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
action := r.FormValue("action")
|
|
|
|
switch action {
|
|
case "add_domain":
|
|
domain := strings.TrimSpace(r.FormValue("domain"))
|
|
if domain == "" {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
json.NewEncoder(w).Encode(map[string]string{"error": "Domain is required"})
|
|
return
|
|
}
|
|
// Add domain to file
|
|
domains = append(domains, domain)
|
|
writeLines(whitelistPath, domains)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
return
|
|
|
|
case "remove_domain":
|
|
domain := strings.TrimSpace(r.FormValue("domain"))
|
|
// Remove domain from slice
|
|
for i, d := range domains {
|
|
if d == domain {
|
|
domains = append(domains[:i], domains[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
writeLines(whitelistPath, domains)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
|
return
|
|
|
|
case "add_url":
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
url := strings.TrimSpace(r.FormValue("url"))
|
|
if name == "" || url == "" {
|
|
pageData["Notification"] = "Name and URL are required."
|
|
pageData["NotificationType"] = "error"
|
|
} else {
|
|
newURL := URLListItem{Name: name, URL: url, Enabled: true, Group: "Custom"}
|
|
filter.WhitelistURLs = append(filter.WhitelistURLs, newURL)
|
|
updateFilterWhitelistURLs(filterName, filter.WhitelistURLs)
|
|
pageData["Notification"] = "URL added successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.WhitelistURLs
|
|
}
|
|
case "toggle_url":
|
|
idx, _ := strconv.Atoi(r.FormValue("index"))
|
|
if idx >= 0 && idx < len(filter.WhitelistURLs) {
|
|
filter.WhitelistURLs[idx].Enabled = !filter.WhitelistURLs[idx].Enabled
|
|
updateFilterWhitelistURLs(filterName, filter.WhitelistURLs)
|
|
pageData["Notification"] = "URL toggled successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.WhitelistURLs
|
|
}
|
|
case "remove_url":
|
|
idx, _ := strconv.Atoi(r.FormValue("index"))
|
|
if idx >= 0 && idx < len(filter.WhitelistURLs) {
|
|
filter.WhitelistURLs = append(filter.WhitelistURLs[:idx], filter.WhitelistURLs[idx+1:]...)
|
|
updateFilterWhitelistURLs(filterName, filter.WhitelistURLs)
|
|
pageData["Notification"] = "URL removed successfully."
|
|
pageData["NotificationType"] = "success"
|
|
pageData["URLs"] = filter.WhitelistURLs
|
|
}
|
|
}
|
|
}
|
|
|
|
renderTemplate(w, r, "filter-whitelist.html", pageData)
|
|
}
|
|
|