Initial commit
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Relay Admin Panel</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
}
|
||||
h1 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
input, button {
|
||||
padding: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
li {
|
||||
margin: 6px 0;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Relay Admin Panel</h1>
|
||||
|
||||
<label>
|
||||
API Key:
|
||||
<input id="apiKey" type="password" style="width: 100%" />
|
||||
</label>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Registered domains</h3>
|
||||
<button onclick="loadDomains()">Refresh</button>
|
||||
<ul id="domains"></ul>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Add domain</h3>
|
||||
<input id="newDomain" placeholder="example.phone.local" />
|
||||
<button onclick="addDomain()">Add</button>
|
||||
|
||||
<p id="status" class="error"></p>
|
||||
|
||||
<script>
|
||||
const apiBase = "/domains";
|
||||
|
||||
function headers() {
|
||||
return {
|
||||
"apikey": document.getElementById("apiKey").value
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDomains() {
|
||||
const res = await fetch(apiBase, { headers: headers() });
|
||||
if (!res.ok) {
|
||||
showError(res);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
const list = document.getElementById("domains");
|
||||
list.innerHTML = "";
|
||||
|
||||
text.trim().split("\n").forEach(domain => {
|
||||
if (!domain) return;
|
||||
|
||||
const li = document.createElement("li");
|
||||
li.textContent = domain + " ";
|
||||
|
||||
const btn = document.createElement("button");
|
||||
btn.textContent = "Delete";
|
||||
btn.onclick = () => deleteDomain(domain);
|
||||
|
||||
li.appendChild(btn);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function addDomain() {
|
||||
const domain = document.getElementById("newDomain").value;
|
||||
if (!domain) return;
|
||||
|
||||
const res = await fetch(`${apiBase}?domain=${encodeURIComponent(domain)}`, {
|
||||
method: "POST",
|
||||
headers: headers()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
showError(res);
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById("newDomain").value = "";
|
||||
loadDomains();
|
||||
}
|
||||
|
||||
async function deleteDomain(domain) {
|
||||
const res = await fetch(`${apiBase}?domain=${encodeURIComponent(domain)}`, {
|
||||
method: "DELETE",
|
||||
headers: headers()
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
showError(res);
|
||||
return;
|
||||
}
|
||||
|
||||
loadDomains();
|
||||
}
|
||||
|
||||
function showError(res) {
|
||||
document.getElementById("status").textContent =
|
||||
`Error: ${res.status} ${res.statusText}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,21 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var adminLogger *log.Logger
|
||||
|
||||
func InitLogger() {
|
||||
f, err := os.OpenFile(
|
||||
"admin.log",
|
||||
os.O_CREATE|os.O_APPEND|os.O_WRONLY,
|
||||
0600,
|
||||
)
|
||||
if err != nil {
|
||||
log.Fatal("cannot open admin.log:", err)
|
||||
}
|
||||
|
||||
adminLogger = log.New(f, "", log.LstdFlags|log.LUTC)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package admin
|
||||
|
||||
var ApiKey string
|
||||
@@ -0,0 +1,58 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"relay/registry"
|
||||
)
|
||||
|
||||
func domainsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
|
||||
case "GET":
|
||||
registry.Global.Mu.Lock()
|
||||
defer registry.Global.Mu.Unlock()
|
||||
for d := range registry.Global.Domains {
|
||||
w.Write([]byte(d + "\n"))
|
||||
}
|
||||
|
||||
case "POST":
|
||||
domain := strings.ToLower(r.URL.Query().Get("domain"))
|
||||
if domain == "" {
|
||||
http.Error(w, "domain required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
registry.Global.Mu.Lock()
|
||||
if _, exists := registry.Global.Domains[domain]; exists {
|
||||
w.Write([]byte("already registered\n"))
|
||||
} else {
|
||||
registry.Global.Domains[domain] = nil
|
||||
adminLogger.Printf(
|
||||
"ADMIN ADD domain=%s ip=%s",
|
||||
domain, r.RemoteAddr,
|
||||
)
|
||||
w.Write([]byte("registered\n"))
|
||||
}
|
||||
registry.Global.Mu.Unlock()
|
||||
|
||||
case "DELETE":
|
||||
domain := strings.ToLower(r.URL.Query().Get("domain"))
|
||||
|
||||
registry.Global.Mu.Lock()
|
||||
if _, exists := registry.Global.Domains[domain]; exists {
|
||||
delete(registry.Global.Domains, domain)
|
||||
adminLogger.Printf(
|
||||
"ADMIN DELETE domain=%s ip=%s",
|
||||
domain, r.RemoteAddr,
|
||||
)
|
||||
}
|
||||
registry.Global.Mu.Unlock()
|
||||
|
||||
w.Write([]byte("deleted\n"))
|
||||
|
||||
default:
|
||||
http.Error(w, "method not allowed", 405)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func requireAPIKey(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.Header.Get("apikey")
|
||||
ip := r.RemoteAddr
|
||||
|
||||
if key == "" {
|
||||
adminLogger.Printf("ADMIN AUTH_MISSING ip=%s", ip)
|
||||
http.Error(w, "missing api key", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if key != ApiKey {
|
||||
adminLogger.Printf("ADMIN AUTH_FAIL ip=%s", ip)
|
||||
http.Error(w, "invalid api key", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func Serve(addr string) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
mux.Handle("/domains", requireAPIKey(http.HandlerFunc(domainsHandler)))
|
||||
|
||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "admin.html")
|
||||
})
|
||||
|
||||
log.Println("Admin API listening on", addr)
|
||||
log.Fatal(http.ListenAndServeTLS(
|
||||
addr,
|
||||
"certs/admin.crt",
|
||||
"certs/admin.key",
|
||||
mux,
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user