diff --git a/frontend/css/bootstrap.min.css b/api/frontend/css/bootstrap.min.css similarity index 100% rename from frontend/css/bootstrap.min.css rename to api/frontend/css/bootstrap.min.css diff --git a/frontend/css/checkbox.css b/api/frontend/css/checkbox.css similarity index 100% rename from frontend/css/checkbox.css rename to api/frontend/css/checkbox.css diff --git a/frontend/css/index.css b/api/frontend/css/index.css similarity index 100% rename from frontend/css/index.css rename to api/frontend/css/index.css diff --git a/frontend/index.html b/api/frontend/index.html similarity index 100% rename from frontend/index.html rename to api/frontend/index.html diff --git a/frontend/js/bootstrap.min.js b/api/frontend/js/bootstrap.min.js similarity index 100% rename from frontend/js/bootstrap.min.js rename to api/frontend/js/bootstrap.min.js diff --git a/frontend/js/checkbox.js b/api/frontend/js/checkbox.js similarity index 100% rename from frontend/js/checkbox.js rename to api/frontend/js/checkbox.js diff --git a/frontend/js/settings/settings.js b/api/frontend/js/settings/settings.js similarity index 100% rename from frontend/js/settings/settings.js rename to api/frontend/js/settings/settings.js diff --git a/frontend/js/theme.js b/api/frontend/js/theme.js similarity index 100% rename from frontend/js/theme.js rename to api/frontend/js/theme.js diff --git a/api/settings.go b/api/settings.go new file mode 100644 index 0000000..a9f57cf --- /dev/null +++ b/api/settings.go @@ -0,0 +1,35 @@ +package api + +import ( + "dns_service/settings" + "dns_service/utils" + "encoding/json" + "net/http" +) + +func Settings(w http.ResponseWriter, req *http.Request) { + switch req.Method { + case "GET": + respSettings, err := settings.GetSettings() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + utils.RespondWithJSON(w, respSettings, http.StatusOK) + case "POST": + var newSettings map[string]any + if err := json.NewDecoder(req.Body).Decode(&newSettings); err != nil { + http.Error(w, "invalid JSON format", http.StatusBadRequest) + return + } + + respSettings, err := settings.SetSettings(newSettings) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + utils.RespondWithJSON(w, respSettings, http.StatusOK) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/api/settings_file.go b/api/settings_file.go new file mode 100644 index 0000000..aec5afd --- /dev/null +++ b/api/settings_file.go @@ -0,0 +1,45 @@ +package api + +import ( + "dns_service/utils" + "io" + "net/http" + "os" +) + +func SettingsFile(w http.ResponseWriter, req *http.Request) { + configPath := os.Getenv("DNSMASQ_CONFIG") + if configPath == "" { + http.Error(w, "DNSMASQ_CONFIG environment variable not set", http.StatusInternalServerError) + return + } + + switch req.Method { + case "GET": + data, err := os.ReadFile(configPath) + if err != nil { + http.Error(w, "failed to read dnsmasq config file", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/plain") + w.Write(data) + case "POST": + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + http.Error(w, "failed to read request body", http.StatusBadRequest) + return + } + if err := os.WriteFile(configPath, bodyBytes, 0644); err != nil { + http.Error(w, "failed to write dnsmasq config file", http.StatusInternalServerError) + return + } + if err := utils.RestartDnsmasq(); err != nil { + http.Error(w, "failed to restart dnsmasq", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "text/plain") + w.Write(bodyBytes) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} diff --git a/api/spa.go b/api/spa.go new file mode 100644 index 0000000..c75186a --- /dev/null +++ b/api/spa.go @@ -0,0 +1,23 @@ +package api + +import ( + "embed" + "fmt" + "net/http" + "path" +) + +//go:embed frontend/* +var Frontend embed.FS + +func SpaHandler(fs http.FileSystem) http.Handler { + fileServer := http.FileServer(fs) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Println("/frontend" + r.URL.Path) + r.URL.Path = "/frontend" + path.Clean(r.URL.Path) + if _, err := fs.Open(path.Clean(r.URL.Path)); err != nil { + r.URL.Path = "/" + } + fileServer.ServeHTTP(w, r) + }) +} diff --git a/main.go b/main.go index a772b96..a0bb875 100644 --- a/main.go +++ b/main.go @@ -1,111 +1,23 @@ package main import ( - "dns_service/settings" - "dns_service/utils" - "embed" - "encoding/json" - "fmt" - "io" + "dns_service/api" "log" "net/http" "os" - "os/exec" - "path" "github.com/joho/godotenv" ) -func Settings(w http.ResponseWriter, req *http.Request) { - if req.Method == "GET" { - resp_settings, err := settings.GetSettings() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - jsondata, err := json.Marshal(resp_settings) - if err != nil { - http.Error(w, "Error json marshal settings", http.StatusInternalServerError) - return - } - w.Header().Add("Content-Type", "application/json") - fmt.Fprintf(w, "%s", string(jsondata)) - } else if req.Method == "POST" { - new_settings := make(map[string]any) - decoder := json.NewDecoder(req.Body) - decoder.Decode(&new_settings) - old_settings, err := settings.GetSettings() - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - resp_settings, err := settings.SetSettings(new_settings, old_settings) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - jsondata, err := json.Marshal(resp_settings) - if err != nil { - http.Error(w, "Error json marshal settings", http.StatusInternalServerError) - return - } - w.Header().Add("Content-Type", "application/json") - fmt.Println(new_settings, old_settings, resp_settings) - fmt.Fprintf(w, "%s", string(jsondata)) - } -} - -func SettingsFile(w http.ResponseWriter, req *http.Request) { - if req.Method == "GET" { - data, e := os.ReadFile(os.Getenv("DNSMASQ_CONFIG")) - if e != nil { - panic(e) - } - w.Header().Add("Content-Type", "text/plain") - fmt.Fprintf(w, "%s", string(data)) - } else if req.Method == "POST" { - bodyBytes, err := io.ReadAll(req.Body) - if err != nil { - http.Error(w, "Error reading request body", http.StatusBadRequest) - return - } - err = os.WriteFile(os.Getenv("DNSMASQ_CONFIG"), bodyBytes, 0644) - if err != nil { - http.Error(w, "Error writing dnsmasq config file", http.StatusInternalServerError) - return - } - fmt.Println(string(bodyBytes)) - utils.RestartDnsmasq() - exec.Command("rc", "restart", "dnsmasq").Run() - w.Header().Add("Content-Type", "text/plain") - fmt.Fprintf(w, "%s", string(bodyBytes)) - } -} - -//go:embed frontend/* -var frontend embed.FS - -func spaHandler(fs http.FileSystem) http.Handler { - fileServer := http.FileServer(fs) - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fmt.Println("/frontend" + r.URL.Path) - r.URL.Path = "/frontend" + path.Clean(r.URL.Path) - if _, err := fs.Open(path.Clean(r.URL.Path)); err != nil { - r.URL.Path = "/" - } - fileServer.ServeHTTP(w, r) - }) -} - func main() { err := godotenv.Load("/etc/dns_service/dns_service.conf") if err != nil { log.Fatal("Error loading .env file") } - http.Handle("/", spaHandler(http.FS(frontend))) - http.HandleFunc("/api/settings", Settings) - http.HandleFunc("/api/settings/file", SettingsFile) + http.Handle("/", api.SpaHandler(http.FS(api.Frontend))) + http.HandleFunc("/api/settings", api.Settings) + http.HandleFunc("/api/settings/file", api.SettingsFile) if os.Getenv("ENABLE_TLS") == "true" { http.ListenAndServeTLS( diff --git a/settings/get_settings.go b/settings/get_settings.go index 3cd4674..168be43 100644 --- a/settings/get_settings.go +++ b/settings/get_settings.go @@ -2,32 +2,51 @@ package settings import ( "dns_service/utils" + "fmt" "os" "strconv" ) func GetSettings() (map[string]any, error) { - settings := make(map[string]any) - data, e := os.ReadFile(os.Getenv("DNSMASQ_CONFIG")) - if e != nil { - return settings, e + configPath := os.Getenv("DNSMASQ_CONFIG") + if configPath == "" { + return nil, fmt.Errorf("DNSMASQ_CONFIG environment variable not set") } + + data, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("failed to read config file at %s: %w", configPath, err) + } + dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=") - if dnsmasqSettings["cache-size"] != "" { - cache_size, _ := strconv.ParseInt(dnsmasqSettings["cache-size"], 10, 64) - settings["cache_size"] = cache_size + settings := make(map[string]any) + + // Parse cache size + if cacheSizeStr, ok := dnsmasqSettings["cache-size"]; ok && cacheSizeStr != "" { + cacheSize, err := strconv.ParseInt(cacheSizeStr, 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse cache-size '%s': %w", cacheSizeStr, err) + } + settings["cache_size"] = cacheSize } - data, e = os.ReadFile(dnsmasqSettings["resolv-file"]) - if e != nil { - return settings, e + + // Read and parse resolv file + if resolvFile, ok := dnsmasqSettings["resolv-file"]; ok { + data, err := os.ReadFile(resolvFile) + if err != nil { + return nil, fmt.Errorf("failed to read resolv-file at %s: %w", resolvFile, err) + } + settings["resolv"] = utils.DecodeResolvs(string(data)) } - resolvSettings := utils.DecodeResolvs(string(data)) - settings["resolv"] = resolvSettings - data, e = os.ReadFile(dnsmasqSettings["addn-hosts"]) - if e != nil { - return settings, e + + // Read and parse hosts file + if hostsFile, ok := dnsmasqSettings["addn-hosts"]; ok { + data, err := os.ReadFile(hostsFile) + if err != nil { + return nil, fmt.Errorf("failed to read addn-hosts file at %s: %w", hostsFile, err) + } + settings["hosts"] = utils.DecodeHosts(string(data)) } - hosts := utils.DecodeHosts(string(data)) - settings["hosts"] = hosts + return settings, nil } diff --git a/settings/set_settings.go b/settings/set_settings.go index 75af1db..731442b 100644 --- a/settings/set_settings.go +++ b/settings/set_settings.go @@ -7,51 +7,86 @@ import ( "strings" ) -func SetSettings(new_settings map[string]any, old_settings map[string]any) (map[string]any, error) { - data, err := os.ReadFile(os.Getenv("DNSMASQ_CONFIG")) +// Helper to write file content +func writeFile(path, content string) error { + return os.WriteFile(path, []byte(content), 0644) +} + +func SetSettings(new_settings map[string]any) (map[string]any, error) { + // 1. Get config path and read the main dnsmasq configuration + configPath := os.Getenv("DNSMASQ_CONFIG") + if configPath == "" { + return nil, fmt.Errorf("DNSMASQ_CONFIG environment variable not set") + } + + data, err := os.ReadFile(configPath) if err != nil { - return old_settings, err + return nil, fmt.Errorf("failed to read config file at %s: %w", configPath, err) } - new_data := strings.Replace( - string(data), - "cache-size="+fmt.Sprintf("%v", old_settings["cache_size"]), - "cache-size="+fmt.Sprintf("%v", new_settings["cache_size"]), - 1, - ) - err = os.WriteFile(os.Getenv("DNSMASQ_CONFIG"), []byte(new_data), 0644) - if err != nil { - return old_settings, err - } - var resolvs string - new_resolvs := new_settings["resolv"].([]any) - for i := range new_resolvs { - resolvs += "nameserver " + new_resolvs[i].(string) + "\n" - } - var hosts string - new_hosts := new_settings["hosts"].(map[string]any) - for key, value := range new_hosts { - hosts += key - switch value.(type) { - case []any: - arrvalue := value.([]any) - for i := range arrvalue { - hosts += " " + arrvalue[i].(string) + dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=") + + // 2. Update cache-size in the main config file + if newCacheSize, ok := new_settings["cache_size"]; ok { + oldCacheSize, oldOk := dnsmasqSettings["cache-size"] + // Replace the line if it exists, otherwise dnsmasq uses a default + if oldOk { + newConfigContent := strings.Replace( + string(data), + "cache-size="+oldCacheSize, + "cache-size="+fmt.Sprintf("%v", newCacheSize), + 1, + ) + if err := writeFile(configPath, newConfigContent); err != nil { + return nil, fmt.Errorf("failed to write updated config to %s: %w", configPath, err) } - hosts += "\n" - break - case string: - hosts += " " + value.(string) + "\n" } } - fmt.Println(resolvs, hosts) - err = os.WriteFile("/etc/dnsmasq.conf.d/resolv.conf", []byte(resolvs), 0644) - if err != nil { - return old_settings, err + + // 3. Update the resolv file + if newResolvs, ok := new_settings["resolv"].([]any); ok { + resolvFile, ok := dnsmasqSettings["resolv-file"] + if !ok { + return nil, fmt.Errorf("resolv-file path not found in dnsmasq config") + } + var b strings.Builder + for _, v := range newResolvs { + if s, ok := v.(string); ok { + fmt.Fprintf(&b, "nameserver %s\n", s) + } + } + if err := writeFile(resolvFile, b.String()); err != nil { + return nil, fmt.Errorf("failed to write to resolv file %s: %w", resolvFile, err) + } } - err = os.WriteFile("/etc/dnsmasq.conf.d/hosts", []byte(hosts), 0644) - if err != nil { - return old_settings, err + + // 4. Update the hosts file + if newHosts, ok := new_settings["hosts"].(map[string]any); ok { + hostsFile, ok := dnsmasqSettings["addn-hosts"] + if !ok { + return nil, fmt.Errorf("addn-hosts path not found in dnsmasq config") + } + var b strings.Builder + for key, value := range newHosts { + b.WriteString(key) + switch v := value.(type) { + case []any: + for _, ip := range v { + fmt.Fprintf(&b, " %s", ip) + } + b.WriteString("\n") + case string: + fmt.Fprintf(&b, " %s\n", v) + } + } + if err := writeFile(hostsFile, b.String()); err != nil { + return nil, fmt.Errorf("failed to write to hosts file %s: %w", hostsFile, err) + } } - utils.RestartDnsmasq() + + // 5. Restart dnsmasq to apply changes + if err := utils.RestartDnsmasq(); err != nil { + return nil, fmt.Errorf("failed to restart dnsmasq: %w", err) + } + return new_settings, nil } diff --git a/utils/decode_hosts.go b/utils/decode_hosts.go index c6e9c34..2623643 100644 --- a/utils/decode_hosts.go +++ b/utils/decode_hosts.go @@ -4,28 +4,21 @@ import "strings" func DecodeHosts(input string) map[string]any { result := make(map[string]any) - pairs := strings.SplitSeq(input, "\n") + lines := strings.Split(input, "\n") - for pair := range pairs { - if pair == "" { + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) == 0 { continue } - kv := strings.Split(pair, " ") - if len(kv) == 2 { - key := strings.TrimSpace(kv[0]) - value := strings.TrimSpace(kv[1]) - if key != "" { - result[key] = value - } - continue - } - if len(kv) != 0 { - key := strings.TrimSpace(kv[0]) - var value []string - for i := 1; i < len(kv); i++ { - value = append(value, kv[i]) - } - result[key] = value + + key := fields[0] + if len(fields) == 1 { + result[key] = []string{} + } else if len(fields) == 2 { + result[key] = fields[1] + } else { + result[key] = fields[1:] } } return result diff --git a/utils/decode_resolvs.go b/utils/decode_resolvs.go index 81984ae..fd7c53c 100644 --- a/utils/decode_resolvs.go +++ b/utils/decode_resolvs.go @@ -4,19 +4,15 @@ import "strings" func DecodeResolvs(input string) []string { var result []string - pairs := strings.SplitSeq(input, "\n") + lines := strings.Split(input, "\n") - for pair := range pairs { - if pair == "" { - continue - } - kv := strings.SplitN(pair, " ", 2) - if len(kv) != 2 { - continue - } - value := strings.TrimSpace(kv[1]) - if value != "" { - result = append(result, value) + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + if strings.HasPrefix(trimmedLine, "nameserver ") { + value := strings.TrimPrefix(trimmedLine, "nameserver ") + if value != "" { + result = append(result, value) + } } } return result diff --git a/utils/decode_settings.go b/utils/decode_settings.go index 6685560..581d07e 100644 --- a/utils/decode_settings.go +++ b/utils/decode_settings.go @@ -4,20 +4,22 @@ import "strings" func DecodeSettings(input string, pairDelim string, kvDelim string) map[string]string { result := make(map[string]string) - pairs := strings.SplitSeq(input, pairDelim) + pairs := strings.Split(input, pairDelim) - for pair := range pairs { - if pair == "" { + for _, pair := range pairs { + trimmedPair := strings.TrimSpace(pair) + if trimmedPair == "" { continue } - kv := strings.SplitN(pair, kvDelim, 2) + + kv := strings.SplitN(trimmedPair, kvDelim, 2) if len(kv) != 2 { continue } + key := strings.TrimSpace(kv[0]) - value := strings.TrimSpace(kv[1]) if key != "" { - result[key] = value + result[key] = strings.TrimSpace(kv[1]) } } return result diff --git a/utils/response_json.go b/utils/response_json.go new file mode 100644 index 0000000..f7ddfa8 --- /dev/null +++ b/utils/response_json.go @@ -0,0 +1,18 @@ +package utils + +import ( + "encoding/json" + "net/http" +) + +// Helper to write JSON responses +func RespondWithJSON(w http.ResponseWriter, data any, statusCode int) { + jsonData, err := json.Marshal(data) + if err != nil { + http.Error(w, "failed to marshal JSON", http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(statusCode) + w.Write(jsonData) +} diff --git a/utils/restart_dnsmasq.go b/utils/restart_dnsmasq.go index c7ec94f..bb286f6 100644 --- a/utils/restart_dnsmasq.go +++ b/utils/restart_dnsmasq.go @@ -1,17 +1,26 @@ package utils import ( + "fmt" "os" "os/exec" ) -func RestartDnsmasq() { +func RestartDnsmasq() error { + var cmd *exec.Cmd switch os.Getenv("SYSTEM_INITIALIZATION") { case "rc": - exec.Command("rc", "restart", "dnsmasq").Run() + cmd = exec.Command("rc", "restart", "dnsmasq") case "openrc": - exec.Command("rc-service", "dnsmasq", "restart").Run() + cmd = exec.Command("rc-service", "dnsmasq", "restart") case "systemd": - exec.Command("systemctl", "restart", "dnsmasq").Run() + cmd = exec.Command("systemctl", "restart", "dnsmasq") + default: + return fmt.Errorf("unsupported system initialization: %s", os.Getenv("SYSTEM_INITIALIZATION")) } + + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to restart dnsmasq: %w", err) + } + return nil }