more optimizations #4

Merged
wt merged 1 commits from actions into main 2025-07-07 22:41:18 +07:00
19 changed files with 276 additions and 189 deletions
+35
View File
@@ -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)
}
}
+45
View File
@@ -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)
}
}
+23
View File
@@ -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)
})
}
+4 -92
View File
@@ -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(
+36 -17
View File
@@ -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
}
+74 -39
View File
@@ -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
}
+12 -19
View File
@@ -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
+8 -12
View File
@@ -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
+8 -6
View File
@@ -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
+18
View File
@@ -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)
}
+13 -4
View File
@@ -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
}