added multiple pages and API calls for them
parent
f76004a261
commit
2ebe43e886
|
|
@ -1,6 +0,0 @@
|
|||
/node_modules/
|
||||
/public/build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -34,6 +34,7 @@
|
|||
"fa-svelte": "^3.1.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
"sirv-cli": "^1.0.0",
|
||||
"svelte-awesome": "^2.3.1",
|
||||
"svelte-chartjs": "^1.0.1",
|
||||
"svelte-routing": "^1.6.0",
|
||||
"sveltestrap": "^4.2.1"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
|
||||
<title>Svelte app</title>
|
||||
<title>Gopherhole: A customizable DNS server</title>
|
||||
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="stylesheet" href="/build/bundle.css" />
|
||||
|
|
|
|||
|
|
@ -1,21 +1,63 @@
|
|||
<script lang="ts">
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Tailwind from "./Tailwind.svelte";
|
||||
import type { SvelteComponent } from "svelte";
|
||||
import { Router, Route } from "svelte-routing";
|
||||
|
||||
import {
|
||||
faChartLine,
|
||||
faClipboardList,
|
||||
faCloudUploadAlt,
|
||||
faTrafficLight,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { Router, Link, Route } from "svelte-routing";
|
||||
import Home from "./routes/Home.svelte";
|
||||
import Navbar from "./components/Navbar.svelte";
|
||||
import Rules from "./routes/Rules.svelte";
|
||||
import RuleLists from "./routes/RuleLists.svelte";
|
||||
import Recursors from "./routes/Recursors.svelte";
|
||||
|
||||
export let url = "";
|
||||
|
||||
interface NavLink {
|
||||
label: string;
|
||||
to: string;
|
||||
icon: IconDefinition;
|
||||
Component: typeof SvelteComponent;
|
||||
}
|
||||
|
||||
const links: NavLink[] = [
|
||||
{ label: "Stats", to: "/", icon: faChartLine, Component: Home },
|
||||
{
|
||||
label: "Rules",
|
||||
to: "/rules",
|
||||
icon: faTrafficLight,
|
||||
Component: Rules,
|
||||
},
|
||||
{
|
||||
label: "Rule Lists",
|
||||
to: "/rulelists",
|
||||
icon: faClipboardList,
|
||||
Component: RuleLists,
|
||||
},
|
||||
{
|
||||
label: "Recursors",
|
||||
to: "/recursors",
|
||||
icon: faCloudUploadAlt,
|
||||
Component: Recursors,
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<Tailwind />
|
||||
<main class="flex w-full h-full">
|
||||
<Router {url}>
|
||||
<nav class="bg-gray-700 text-gray-300 w-1/8 py-5 px-2 shadow-md">
|
||||
<Link to="/">Home</Link>
|
||||
</nav>
|
||||
<Navbar {links} />
|
||||
<div class="flex-column py-5 px-5 w-full h-full overflow-y-auto">
|
||||
<Route path="/" component={Home} />
|
||||
{#each links as link}
|
||||
<Route path={link.to} component={link.Component} />
|
||||
{/each}
|
||||
</div>
|
||||
</Router>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { sub, format } from 'date-fns';
|
||||
import { readable } from "svelte/store";
|
||||
|
||||
export const formatDate = date => format(date, "yyyy-MM-dd HH:mm:ss.SSS");
|
||||
export const SearchDateFormatStr = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
export const formatDateForSearch = date => format(date, SearchDateFormatStr);
|
||||
|
||||
/*
|
||||
take something like `clientIp:127.0.0.1 protocol:udp domain:google.com`
|
||||
|
|
@ -25,9 +26,9 @@ export const fetchMetrics = async function ({
|
|||
} = {}) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`http://localhost:8080/api/v1/metrics/stats?start=${formatDate(
|
||||
`http://localhost:8080/api/v1/metrics/stats?start=${formatDateForSearch(
|
||||
start
|
||||
)}&end=${formatDate(end)}&key=${key}&interval=${interval}`,
|
||||
)}&end=${formatDateForSearch(end)}&key=${key}&interval=${interval}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { Accept: "application/json" }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
import { apiCall } from './util'
|
||||
|
||||
export interface Recursor {
|
||||
ipAddress: string
|
||||
timeoutMs: number
|
||||
weight: number
|
||||
}
|
||||
|
||||
export const getRecursors = () => apiCall<Recursor>('recursors')
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { apiCall } from './util'
|
||||
|
||||
export interface RuleAnswer {
|
||||
type: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface Rule {
|
||||
id: number
|
||||
weight: number
|
||||
enabled: boolean
|
||||
created: string
|
||||
name: string
|
||||
value: string
|
||||
answer: RuleAnswer
|
||||
ttl: number
|
||||
}
|
||||
|
||||
export const getRules = () => apiCall<Rule>('rules');
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
interface APIResponse<T> {
|
||||
success?: boolean
|
||||
payload?: T[]
|
||||
error?: string
|
||||
|
||||
}
|
||||
|
||||
export const API_HOST = `http://localhost:8080/api/v1`;
|
||||
|
||||
export const apiCall = async function<T>(url: string, method: string = 'GET'): Promise<APIResponse<T>> {
|
||||
try {
|
||||
const data = await fetch(`${API_HOST}/${url}`, {
|
||||
headers: { "Accept": "application/json" },
|
||||
method,
|
||||
});
|
||||
|
||||
const { success, payload } = await data.json();
|
||||
|
||||
if (success) {
|
||||
return { payload };
|
||||
}
|
||||
|
||||
return { error: payload };
|
||||
|
||||
} catch (error) {
|
||||
console.trace(`API CALL FAILED - ${url}: ${error}`);
|
||||
return { error };
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +1,60 @@
|
|||
<script lang="ts">
|
||||
import { format, parse } from "date-fns";
|
||||
|
||||
import {
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupText,
|
||||
} from "sveltestrap";
|
||||
export let label = "label";
|
||||
import { SearchDateFormatStr } from "../api/metrics";
|
||||
|
||||
interface IDateTimeParts {
|
||||
date: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export let label: string = "label";
|
||||
export let defaultValue: Date = new Date();
|
||||
|
||||
const fromDateTimeParts = ({ date, time }: IDateTimeParts): Date =>
|
||||
parse(`${date} ${time}`, SearchDateFormatStr, new Date());
|
||||
|
||||
const toDateTimeParts = (dt: Date = new Date()): IDateTimeParts => ({
|
||||
date: format(dt, "yyyy-MM-dd"),
|
||||
time: format(dt, "HH:mm:ss"),
|
||||
});
|
||||
|
||||
const isDate = (v) => (v.match(/[\d]{4}-\d{2}-\d{2}/) || []).length > 0;
|
||||
|
||||
let dateTimeParts = toDateTimeParts(defaultValue);
|
||||
|
||||
const update = ({ target: { value } }) => {
|
||||
const { date, time } = dateTimeParts;
|
||||
let dateTimePartsInput = isDate(value)
|
||||
? { date: value, time }
|
||||
: { date, time: value };
|
||||
|
||||
const updatedDateTime = fromDateTimeParts(dateTimePartsInput);
|
||||
dateTimeParts = toDateTimeParts(updatedDateTime);
|
||||
console.log(dateTimeParts);
|
||||
};
|
||||
</script>
|
||||
|
||||
<InputGroup class="flex flex-row">
|
||||
<InputGroupAddon addonType="prepend">
|
||||
<InputGroupText>{label}</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
<Input bsSize="sm" type="date" />
|
||||
<Input bsSize="sm" type="time" />
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="date"
|
||||
value={dateTimeParts.date}
|
||||
on:change={update}
|
||||
/>
|
||||
<Input
|
||||
bsSize="sm"
|
||||
type="time"
|
||||
value={dateTimeParts.time}
|
||||
on:change={update}
|
||||
/>
|
||||
</InputGroup>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
onMount(async () => {
|
||||
try {
|
||||
const { payload } = await getLogs();
|
||||
console.log(payload);
|
||||
rows = payload || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
<script lang="ts">
|
||||
import Icon from "svelte-awesome";
|
||||
import { Link } from "svelte-routing";
|
||||
|
||||
import type { SvelteComponent } from "svelte";
|
||||
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
interface NavLink {
|
||||
label: string;
|
||||
to: string;
|
||||
icon: IconDefinition;
|
||||
Component: typeof SvelteComponent;
|
||||
}
|
||||
|
||||
export let links: NavLink[] = [];
|
||||
$: currentPath = location.pathname;
|
||||
</script>
|
||||
|
||||
<nav class="bg-gray-700 text-gray-300 w-1/8 py-5 px-2 shadow-md">
|
||||
<ul class="m-0 p-0 h-full">
|
||||
{#each links as l}
|
||||
<li class:selected={l.to === currentPath} class="link-item">
|
||||
<Link
|
||||
class="flex flex-col justify-center text-gray-300 hover:text-gray-500 no-underline"
|
||||
to={l.to}
|
||||
>
|
||||
<Icon
|
||||
class="w-full text-gray-300"
|
||||
data={l.icon}
|
||||
label={l.label}
|
||||
scale="1.75"
|
||||
/>
|
||||
<span class="text-sm">{l.label}</span>
|
||||
</Link>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style lang="postcss">
|
||||
.link-item {
|
||||
@apply my-5 py-2 flex justify-center no-underline;
|
||||
}
|
||||
|
||||
.link-item span {
|
||||
margin-top: 5px;
|
||||
@apply no-underline;
|
||||
}
|
||||
|
||||
/* .selected span {
|
||||
text-decoration: underline !important;
|
||||
@apply text-white underline;
|
||||
} */
|
||||
</style>
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Column, Row, Table } from "sveltestrap";
|
||||
import { getRules } from "../api/rules";
|
||||
import type { Rule } from "../api/rules";
|
||||
|
||||
let rows: Rule[] = [];
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { payload } = await getRules();
|
||||
console.log(payload);
|
||||
rows = payload || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-column text-sm">
|
||||
{#if rows.length > 0}
|
||||
<Table {rows} let:row hover bordered>
|
||||
<Column header="Enabled">{row.enabled}</Column>
|
||||
<Column header="Weight">{row.weight}</Column>
|
||||
<Column header="Name">{row.name}</Column>
|
||||
<Column header="Expressions">{row.value}</Column>
|
||||
<Column header="Answer"
|
||||
>{row.answer.value} - {row.answer.type}</Column
|
||||
>
|
||||
<Column header="TTL">{row.ttl}</Column>
|
||||
</Table>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -9,8 +9,6 @@
|
|||
const ctx = canvas.getContext("2d");
|
||||
const { labels, datasets } = await fetchAndPrepareMetrics();
|
||||
|
||||
console.log(labels);
|
||||
console.log(datasets);
|
||||
var c = new Chart(ctx, {
|
||||
type: "line",
|
||||
data: { labels, datasets },
|
||||
|
|
|
|||
|
|
@ -2,19 +2,18 @@
|
|||
import LogViewer from "../components/LogViewer.svelte";
|
||||
import SearchOptions from "../components/SearchOptions.svelte";
|
||||
import TimeChart from "../components/TimeChart.svelte";
|
||||
import PageContainer from "./PageContainer.svelte";
|
||||
</script>
|
||||
|
||||
<section class="w-full">
|
||||
<header>
|
||||
<h1>Home</h1>
|
||||
</header>
|
||||
<section>
|
||||
<SearchOptions />
|
||||
</section>
|
||||
<section class="m-10">
|
||||
<PageContainer
|
||||
header="Stats"
|
||||
description="Server performance stats and activity log"
|
||||
>
|
||||
<SearchOptions />
|
||||
<section class="my-5">
|
||||
<TimeChart />
|
||||
</section>
|
||||
<section>
|
||||
<section class="my-5">
|
||||
<LogViewer />
|
||||
</section>
|
||||
</section>
|
||||
</PageContainer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">
|
||||
import { fade, fly } from "svelte/transition";
|
||||
export let header: string = "Home";
|
||||
export let description: string = "";
|
||||
</script>
|
||||
|
||||
<section out:fade={{ duration: 100 }} class="w-full">
|
||||
<header class="flex flex-column">
|
||||
<h1 in:fade={{ duration: 500 }}>
|
||||
{header}
|
||||
</h1>
|
||||
<span in:fly={{ y: 150, delay: 50, duration: 150 }}>{description}</span>
|
||||
</header>
|
||||
<section in:fade={{ delay: 150, duration: 550 }} class="py-5">
|
||||
<slot>
|
||||
<em>This page is (un?)intentionally left blank.</em>
|
||||
</slot>
|
||||
</section>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import PageContainer from "./PageContainer.svelte";
|
||||
|
||||
import { onMount } from "svelte";
|
||||
import { Column, Row, Table } from "sveltestrap";
|
||||
import { getRecursors } from "../api/recursors";
|
||||
import type { Recursor } from "../api/recursors";
|
||||
|
||||
let rows: Recursor[] = [];
|
||||
onMount(async () => {
|
||||
try {
|
||||
const { payload } = await getRecursors();
|
||||
console.log(payload);
|
||||
rows = payload || [];
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<PageContainer
|
||||
header="Recursors"
|
||||
description="List of upstreams servers to use for resolving DNS records"
|
||||
>
|
||||
<section class="flex flex-column text-sm">
|
||||
{#if rows.length > 0}
|
||||
<Table {rows} let:row hover bordered>
|
||||
<Column header="IP Address">{row.ipAddress}</Column>
|
||||
<Column header="Weight">{row.weight}</Column>
|
||||
<Column header="Timeout in MS">{row.timeoutMs}</Column>
|
||||
</Table>
|
||||
{/if}
|
||||
</section>
|
||||
</PageContainer>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
<script lang="ts">
|
||||
import PageContainer from "./PageContainer.svelte";
|
||||
</script>
|
||||
|
||||
<PageContainer
|
||||
header="Rule Lists"
|
||||
description="Import rule lists for maintenance free Ad blocking"
|
||||
/>
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<script lang="ts">
|
||||
import RulesViewer from "../components/RulesViewer.svelte";
|
||||
import PageContainer from "./PageContainer.svelte";
|
||||
</script>
|
||||
|
||||
<PageContainer
|
||||
header="Rules"
|
||||
description="Specify rules to alter DNS resolution behavior"
|
||||
>
|
||||
<RulesViewer />
|
||||
</PageContainer>
|
||||
|
|
@ -46,9 +46,7 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
|
|||
ql.RecurseUpstreamIP = resolved.UpstreamUsed
|
||||
ql.RecurseRoundTripTimeMs = int(resolved.RoundtripTime.Milliseconds())
|
||||
ql.Status = RecursedUpstream
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
ql.Error = err.Error()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
|
|||
handler.Use(middleware.RealIP)
|
||||
handler.Use(middleware.Logger)
|
||||
handler.Use(middleware.AllowContentType("application/json; utf-8", "application/json"))
|
||||
handler.Use(middleware.Timeout(time.Second * 10))
|
||||
handler.Use(middleware.Timeout(time.Second * 5))
|
||||
|
||||
handler.Use(cors.Handler(cors.Options{
|
||||
AllowedOrigins: []string{"http://*", "https://*"},
|
||||
|
|
@ -53,6 +53,8 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
|
|||
r.Get("/rules", RestHandler(a.getRules).ToHF())
|
||||
r.Put("/rules", RestHandler(a.createRule).ToHF())
|
||||
r.Delete("/rules/{id:[0-9]+}", RestHandler(a.deleteRule).ToHF())
|
||||
r.Get("/recursors", RestHandler(a.getRecursors).ToHF())
|
||||
r.Put("/recursors", RestHandler(a.addRecursor).ToHF())
|
||||
// r.Put("/rules/lists", a.addRulelist)
|
||||
// r.Get("/rules/lists", a.getRuleLists)
|
||||
// r.Delete("/rules/lists/{id}", a.deleteRuleList)
|
||||
|
|
@ -71,6 +73,54 @@ func (a *adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
a.h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (a *adminHandler) addRecursor(r *http.Request) (*RestResponse, error) {
|
||||
var recursorHttpInput RecursorRow
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&recursorHttpInput); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ipAddr, port, ok := recursorHttpInput.ValidIp(); ok {
|
||||
if err := a.Storage.AddRecursors(ipAddr, port, recursorHttpInput.TimeoutMs, recursorHttpInput.Weight); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &RestResponse{
|
||||
Payload: struct {
|
||||
Success bool "json:\"success\""
|
||||
Payload interface{} "json:\"payload\""
|
||||
}{
|
||||
Success: true,
|
||||
Payload: recursorHttpInput,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *adminHandler) getRecursors(r *http.Request) (*RestResponse, error) {
|
||||
recursors, err := a.Storage.GetRecursors()
|
||||
if err != nil {
|
||||
return &RestResponse{
|
||||
Payload: struct {
|
||||
Success bool "json:\"success\""
|
||||
Payload interface{} "json:\"payload\""
|
||||
}{
|
||||
Payload: err.Error(),
|
||||
},
|
||||
}, err
|
||||
}
|
||||
|
||||
return &RestResponse{
|
||||
Payload: struct {
|
||||
Success bool "json:\"success\""
|
||||
Payload interface{} "json:\"payload\""
|
||||
}{
|
||||
Success: true,
|
||||
Payload: recursors,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *adminHandler) deleteRule(r *http.Request) (*RestResponse, error) {
|
||||
ruleIdParam := chi.URLParam(r, "id")
|
||||
ruleId, err := strconv.Atoi(ruleIdParam)
|
||||
|
|
@ -305,7 +355,12 @@ func (rr *RestResponse) Write(w http.ResponseWriter) error {
|
|||
type RestHandler func(request *http.Request) (*RestResponse, error)
|
||||
|
||||
func (rh RestHandler) ToHF() http.HandlerFunc {
|
||||
return func(rw http.ResponseWriter, r *http.Request) { rh.ServeHTTP(rw, r) }
|
||||
return func(rw http.ResponseWriter, r *http.Request) {
|
||||
|
||||
rid := r.Context().Value(middleware.RequestIDKey)
|
||||
rw.Header().Set(middleware.RequestIDHeader, rid.(string))
|
||||
rh.ServeHTTP(rw, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rh RestHandler) Error(e error) *RestResponse {
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ type Rule struct {
|
|||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
Answer struct {
|
||||
Type RuleType
|
||||
Value string
|
||||
Type RuleType `json:"type"`
|
||||
Value string `json:"value"`
|
||||
} `json:"answer"`
|
||||
TTL int `json:"ttl"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
|
@ -75,6 +77,29 @@ type RecursorRow struct {
|
|||
Weight int `json:"weight"`
|
||||
}
|
||||
|
||||
func (rr RecursorRow) ValidIp() (net.IP, int, bool) {
|
||||
ipAddrFrags := strings.Split(rr.IpAddress, ":")
|
||||
if len(ipAddrFrags) == 0 || len(ipAddrFrags) > 2 {
|
||||
return nil, -1, false
|
||||
}
|
||||
|
||||
var err error
|
||||
var parsedIp net.IP
|
||||
parsedPort := -1
|
||||
|
||||
if parsedIp = net.ParseIP(ipAddrFrags[0]); parsedIp == nil {
|
||||
return nil, -1, false
|
||||
}
|
||||
|
||||
if len(ipAddrFrags) > 1 {
|
||||
if parsedPort, err = strconv.Atoi(ipAddrFrags[1]); err != nil {
|
||||
return parsedIp, -1, false
|
||||
}
|
||||
}
|
||||
|
||||
return parsedIp, parsedPort, true
|
||||
}
|
||||
|
||||
func (ss *Sqlite) AddRecursors(ip net.IP, port, timeout, weight int) error {
|
||||
sql := `INSERT INTO recursors (ipAddress, timeoutMs, weight) VALUES (?, ?, ?);`
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue