initial commit

This commit is contained in:
wt committed 2025-06-26 18:20:20 +07:00
commit 5ba52dd285
19 files changed
+639

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
dns_service
go.sum
+10
View File
@@ -0,0 +1,10 @@
DNSMASQ_CONFIG=/etc/dnsmasq.conf
ENABLE_TLS=true
CERT_FILE=/etc/dns_service/dns.home.com.crt
KEY_FILE=/etc/dns_service/dns.home.com.key
SERVER_ADDRESS=0.0.0.0
SERVER_PORT=8090
SYSTEM_INITIALIZATION=rc
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
#--- import colors and init-funcs ---
. /usr/lib/init/colors.sh
. /usr/lib/init/init-funcs.sh
#------------------------------------
start() {
notify "Starting dns_service"
/usr/bin/dns_service &> /dev/null &
echo $(pgrep dns_service) > /var/run/dns_service.pid
check $? "start dns_service"
return $?
}
stop() {
notify "Stopping dns_service"
kill $(cat /var/run/dns_service.pid)
check $? "stop dns_service"
rm /var/run/dns_service.pid &> /dev/null
return $?
}
case "$1" in
start|stop|restart|reload|help)
"$1"
;;
*)
help
;;
esac
return $?
+6
View File
@@ -0,0 +1,6 @@
@charset "UTF-8";/*!
* Bootstrap v5.3.5 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherLine truncated
/*# sourceMappingURL=bootstrap.min.css.map */
+70
View File
@@ -0,0 +1,70 @@
.checkbox-wrapper-2 {
background-color: transparent;
}
.checkbox-wrapper-2 .ikxBAC {
appearance: none;
background-color: #dfe1e4;
border-radius: 72px;
border-style: none;
flex-shrink: 0;
height: 20px;
margin: 0;
position: relative;
width: 30px;
transition: 0.3s;
}
.checkbox-wrapper-2 .ikxBAC::before {
bottom: -6px;
content: "";
left: -6px;
position: absolute;
right: -6px;
top: -6px;
transition: 0.3s;
cursor: pointer;
}
.checkbox-wrapper-2 .ikxBAC,
.checkbox-wrapper-2 .ikxBAC::after {
transition: 300ms;
}
.checkbox-wrapper-2 .ikxBAC::after {
background-color: #fff;
border-radius: 50%;
content: "";
height: 14px;
left: 3px;
position: absolute;
top: 3px;
width: 14px;
cursor: pointer;
}
.checkbox-wrapper-2 input[type="checkbox"] {
cursor: default;
}
.checkbox-wrapper-2 .ikxBAC:hover {
background-color: #c9cbcd;
transition-duration: 0s;
}
.checkbox-wrapper-2 .ikxBAC:checked {
background-color: green;
}
.checkbox-wrapper-2 .ikxBAC:checked::after {
background-color: #fff;
left: 13px;
}
.checkbox-wrapper-2 :focus:not(.focus-visible) {
outline: 0;
}
.checkbox-wrapper-2 .ikxBAC:checked:hover {
background-color: hsl(from green h s 20%);
}
+20
View File
@@ -0,0 +1,20 @@
* {
transition: 0.3s;
}
.hidden {
transform: scale(0);
height: 0;
padding: 0;
margin: 0;
overflow: hidden;
opacity: 0;
}
#save-settings-button {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 10px;
}
+72
View File
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="description" content="" />
<meta name="author" content="" />
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, width=device-width"/>
<link rel="stylesheet" href="css/index.css" />
<link rel="stylesheet" href="css/checkbox.css" />
<link rel="stylesheet" href="css/bootstrap.min.css">
<title>Dns Service</title>
</head>
<body>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container-fluid">
<div class="d-flex align-items-center gap-2">
<a href="/" class="navbar-brand">Dns Service</a>
<p style="margin-bottom: 0;">Dark theme</p>
<div class="checkbox-wrapper-2"><input type="checkbox" class="sc-gJwTLC ikxBAC" id="dark_theme"/></div>
</div>
</div>
</nav>
<section class="settings-section d-grid gap-2 justify-content-center p-3">
<div class="card">
<div class="card-header"><h3 class="card-title">Upstream Dns Servers</h3></div>
<div class="card-body">
<table id="upstream-dns-servers-table" class="table">
<thead><tr><th>Upstream Dns Servers</th></tr></thead>
<tbody id="upstream-dns-servers-table-body"></tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header"><h3 class="card-title">Local Domains</h3></div>
<div class="card-body">
<table class="local-domains-table table">
<thead><tr><th>IP</th><th>Domain</th><th>Delete</th></tr></thead>
<tbody id="local-domains-table-body">
</tbody>
</table>
</div>
</div>
<div class="card">
<div class="card-header d-flex align-items-center gap-3 justify-content-between">
<h3>DHCP</h3>
<div class="checkbox-wrapper-2"><input type="checkbox" class="sc-gJwTLC ikxBAC" id="dhcp_enabled"/></div>
</div>
<div class="card-body hidden" id="dhcp_rect_body">
<h3 class="card-title">DHCP range</h3>
<div class="d-flex align-items-center gap-2">
<input placeholder="192.168.0.2" type="text" id="start-dhcp-range" class="form-control"/>
-
<input placeholder="192.168.0.254" type="text" id="stop-dhcp-range" class="form-control"/>
</div>
</div>
</div>
<div class="card" style="margin-bottom: 50px;">
<div class="card-header d-flex align-items-center gap3 justify-content-between">
<h3>Cache size</h3>
</div>
<div class="card-body">
<input type="number" name="" id="cache-size-input" class="form-control">
</div>
</div>
</section>
<button class="btn btn-primary" id="save-settings-button">Save</button>
<script src="js/checkbox.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/theme.js"></script>
<script src="js/settings/settings.js"></script>
</body>
</html>
+7
View File
@@ -0,0 +1,7 @@
/*!
* Bootstrap v5.3.5 (https://getbootstrap.com/)
* Copyright 2011-2025 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
*/
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("@popperjs/core")):"function"==typeof define&&define.amd?define(["@popperjs/core"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e(t.Popper)}(this,(function(t){"use strict";function e(t){const e=Object.create(null,{[Symbol.toStringTag]:{value:"Module"}});if(t)for(const i in t)if("default"!==i){const s=Object.getOwnPropertyDescriptor(t,i);Object.defineProperty(e,i,s.get?s:{enumerable:!0,get:()=>t[i]})}return e.default=t,Object.freeze(e)}const i=e(t),s=new Map,n={set(t,e,i){s.has(t)||s.set(t,new Map);const n=s.get(t);n.has(e)||0===n.size?n.set(e,i):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(n.keys())[0]}.`)},get:(t,e)=>s.has(t)&&s.get(t).get(e)||null,remove(t,e){if(!s.has(t))return;const i=s.get(t);i.delete(e),0===i.size&&s.delete(t)}},o="transitionend",r=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s"#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),a=t=>{t.dispatchEvent(new Event(o))},l=t=>!(!t||"object"!=typeof t)&&(void 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),c=t=>l(t)?t.jquery?t[0]:t:"string"==typeof t&&t.length>0?document.querySelector(r(t)):null,h=t=>{if(!l(t)||0===t.getClientRects().length)return!1;const e="visible"===getComputedStyle(t).getPropertyValue("visibility"),i=t.closest("details:not([open])");if(!i)return e;if(i!==t){const e=t.closest("summary");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},d=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains("disabled")||(void 0!==t.disabled?t.disabled:t.hasAttribute("disabled")&&"false"!==t.getAttribute("disabled")),u=t=>{if(!document.documentElement.attachShadow)return null;if("function"==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?u(t.parentNode):null},_=()=>{},g=t=>{t.offsetHeight},f=()=>window.jQuery&&!document.body.hasAttribute("data-bs-no-jquery")?window.jQuery:null,m=[],p=()=>"rtl"===document.documentElement.dir,b=t=>{var e;e=()=>{const e=f();if(e){const i=t.NAME,s=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t,e.fn[i].noConflict=()=>(e.fn[i]=s,t.jQueryInterface)}},"loading"===document.readyState?(m.length||document.addEventListener("DOMContentLoaded",(()=>{for(const t of m)t()})),m.push(e)):e()},v=(t,e=[],i=t)=>"function"==typeof t?t.call(...e):i,y=(t,e,i=!0)=>{if(!i)return void v(t);const s=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const s=Number.parseFloat(e),n=Number.parseFloat(i);return s||n?(e=e.split(",")[0],i=i.split(",")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5;let n=!1;const r=({target:i})=>{i===e&&(n=!0,e.removeEventListener(o,r),v(t))};e.addEventListener(o,r),setTimeout((()=>{n||a(e)}),s)},w=(t,e,i,s)=>{const n=t.length;let o=t.indexOf(e);return-1===o?!i&&s?t[n-1]:t[0]:(o+=i?1:-1,s&&(o=(o+n)%n),t[Math.max(0,Math.min(o,n-1))])},A=/[^.]*(?=\..*)\.|.*/,E=/\..*/,C=/::\d+$/,T={};let k=1;const $={mouseenter:"mouseover",mouseleave:"mouseout"},S=new Set(["click","dblclick","mouseup","mousedown","contextmenu","mousewheel","DOMMouseScroll","mouseover","mouseout","mousemove","selectstart","selectend","keydown","keypress","keyup","orientationchange","touchstart","touchmove","touchend","touchcancel","pointerdown","pointermove","pointerup","pointerleave","pointercancel","gesturestart","gesturechange","gestureend","focus","blur","change","reset","select","submit","focusin","focusout","load","unload","beforeunload","resize","move","DOMContentLoaded","readystatechange","error","abort","scroll"]);function L(t,e){return e&&`${e}::${k++}`||t.uidEvent||k++}function O(t){const e=L(t);return t.uidEvent=e,T[e]=T[e]||{},T[e]}function I(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function D(t,e,i){const s="string"==typeof e,n=s?i:e||i;let o=M(t);return S.has(o)||(o=t),[s,n,o]}function N(t,e,i,s,n){if("string"!=typeof e||!t)return;let[o,r,a]=D(e,i,s);if(e in $){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const l=O(t),c=l[a]||(l[a]={}),h=I(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&n);const d=L(r,e.replace(A,"")),u=o?function(t,e,i){return function s(n){const o=t.querySelectorAll(e);for(let{target:r}=n;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return F(n,{delegateTarget:r}),s.oneOff&&j.off(t,n.type,e,i),i.apply(r,[n])}}(t,i,r):function(t,e){return function i(s){return F(s,{delegateTarget:t}),i.oneOff&&j.off(t,s.type,e),e.apply(t,[s])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=n,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function P(t,e,i,s,n){const o=I(e[i],s,n);o&&(t.removeEventListener(i,o,Boolean(n)),delete e[i][o.uidEvent])}function x(t,e,i,s){const n=e[i]||{};for(const[o,r]of Object.entries(n))o.includes(Line truncated
//# sourceMappingURL=bootstrap.min.js.map
+8
View File
@@ -0,0 +1,8 @@
const dhcp_enabled = document.querySelector("#dhcp_enabled");
const dhcp_rect_body = document.querySelector("#dhcp_rect_body");
dhcp_enabled.addEventListener("click", () => {
!dhcp_enabled.checked
? dhcp_rect_body.classList.add("hidden")
: dhcp_rect_body.classList.remove("hidden");
});
+77
View File
@@ -0,0 +1,77 @@
const upstream_dns_servers_table_body = document.querySelector('#upstream-dns-servers-table-body')
const local_domains_table_body = document.querySelector('#local-domains-table-body')
const cache_size_input = document.querySelector('#cache-size-input')
const save_settings_button = document.querySelector('#save-settings-button')
var settings = {}
const updateInterface = () => {
upstream_dns_servers_table_body.innerHTML = ""
local_domains_table_body.innerHTML = ""
for (var i = 0; i < settings.resolv.length; i++) {
upstream_dns_servers_table_body.innerHTML += `<tr>
<th class="d-flex align-items-center justify-content-between">
${settings.resolv[i]}
<button class="delete-table-button btn btn-danger">Del</button>
</th>
</tr>`
}
for (const [key, value] of Object.entries(settings.hosts)) {
if (Array.isArray(value)) {
for (var i = 0; i < value.length; i++) {
local_domains_table_body.innerHTML += `<tr>
<th>${key}</th>
<th>${value[i]}</th>
<th><button class="deldomain btn btn-danger" id="${value[i]}">Del</button>
</th>
</tr>`
}
} else {
local_domains_table_body.innerHTML += `<tr>
<th>${key}</th>
<th>${value}</th>
<th><button class="deldomain btn btn-danger" id="${value}">Del</button>
</th>
</tr>`
}
}
cache_size_input.value = settings.cache_size
}
const getSettings = () => {
fetch(window.location.origin + "/api/settings")
.then(data => data.json())
.then(jsondata => {
settings = jsondata
updateInterface()
})
}
const setSettings = () => {
fetch(window.location.origin + "/api/settings", {
method: 'POST',
body: JSON.stringify(settings)
}).then(data => data.json())
.then(jsondata => {
settings = jsondata
updateInterface()
})
}
save_settings_button.addEventListener('click', () => {
setSettings()
})
const write_cache_size = (value) => {
settings.cache_size = value
}
cache_size_input.addEventListener('change', (e) => {
write_cache_size(e.target.value)
})
cache_size_input.addEventListener('input', (e) => {
write_cache_size(e.target.value)
})
getSettings()
+21
View File
@@ -0,0 +1,21 @@
const dark_theme = document.querySelector('#dark_theme')
dark_theme.addEventListener('click', () => {
if (dark_theme.checked) {
document.documentElement.setAttribute('data-bs-theme', 'dark')
localStorage.setItem('data-bs-theme', 'dark')
} else {
document.documentElement.setAttribute('data-bs-theme', 'light')
localStorage.setItem('data-bs-theme', 'light')
}
})
document.addEventListener("DOMContentLoaded", () => {
var theme = localStorage.getItem('data-bs-theme')
if (theme) {
document.documentElement.setAttribute('data-bs-theme', theme)
if (theme === 'dark') {
dark_theme.checked = true
}
}
})
+5
View File
@@ -0,0 +1,5 @@
module dns_service
go 1.24.4
require github.com/joho/godotenv v1.5.1
+123
View File
@@ -0,0 +1,123 @@
package main
import (
"dns_service/settings"
"dns_service/utils"
"embed"
"encoding/json"
"fmt"
"io"
"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)
if os.Getenv("ENABLE_TLS") == "true" {
http.ListenAndServeTLS(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
os.Getenv("CERT_FILE"),
os.Getenv("KEY_FILE"),
nil,
)
} else {
http.ListenAndServe(
os.Getenv("SERVER_ADDRESS")+":"+os.Getenv("SERVER_PORT"),
nil,
)
}
}
+33
View File
@@ -0,0 +1,33 @@
package settings
import (
"dns_service/utils"
"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
}
dnsmasqSettings := utils.DecodeSettings(string(data), "\n", "=")
if dnsmasqSettings["cache-size"] != "" {
cache_size, _ := strconv.ParseInt(dnsmasqSettings["cache-size"], 10, 64)
settings["cache_size"] = cache_size
}
data, e = os.ReadFile(dnsmasqSettings["resolv-file"])
if e != nil {
return settings, e
}
resolvSettings := utils.DecodeResolvs(string(data))
settings["resolv"] = resolvSettings
data, e = os.ReadFile(dnsmasqSettings["addn-hosts"])
if e != nil {
return settings, e
}
hosts := utils.DecodeHosts(string(data))
settings["hosts"] = hosts
return settings, nil
}
+57
View File
@@ -0,0 +1,57 @@
package settings
import (
"dns_service/utils"
"fmt"
"os"
"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"))
if err != nil {
return old_settings, 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)
}
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
}
err = os.WriteFile("/etc/dnsmasq.conf.d/hosts", []byte(hosts), 0644)
if err != nil {
return old_settings, err
}
utils.RestartDnsmasq()
return new_settings, nil
}
+32
View File
@@ -0,0 +1,32 @@
package utils
import "strings"
func DecodeHosts(input string) map[string]any {
result := make(map[string]any)
pairs := strings.SplitSeq(input, "\n")
for pair := range pairs {
if pair == "" {
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
}
}
return result
}
+23
View File
@@ -0,0 +1,23 @@
package utils
import "strings"
func DecodeResolvs(input string) []string {
var result []string
pairs := strings.SplitSeq(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)
}
}
return result
}
+24
View File
@@ -0,0 +1,24 @@
package utils
import "strings"
func DecodeSettings(input string, pairDelim string, kvDelim string) map[string]string {
result := make(map[string]string)
pairs := strings.SplitSeq(input, pairDelim)
for pair := range pairs {
if pair == "" {
continue
}
kv := strings.SplitN(pair, kvDelim, 2)
if len(kv) != 2 {
continue
}
key := strings.TrimSpace(kv[0])
value := strings.TrimSpace(kv[1])
if key != "" {
result[key] = value
}
}
return result
}
+17
View File
@@ -0,0 +1,17 @@
package utils
import (
"os"
"os/exec"
)
func RestartDnsmasq() {
switch os.Getenv("SYSTEM_INITIALIZATION") {
case "rc":
exec.Command("rc", "restart", "dnsmasq").Run()
case "openrc":
exec.Command("rc-service", "dnsmasq", "restart").Run()
case "systemd":
exec.Command("systemctl", "restart", "dnsmasq").Run()
}
}