added multiple pages and API calls for them

pull/1/head
Adam Veldhousen 3 years ago
parent f76004a261
commit 2ebe43e886
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

6
client/.gitignore vendored

@ -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…
Cancel
Save