Squashed commit of the following:

commit dd3c21375d3d5a74c4058c5017cd7b4e32039fe1
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sun May 30 20:02:07 2021 -0500

    added manifest file, cleaned up search and time chart

commit 75bb4e2ed29aae571853d7ae8815b4b80b5a2a40
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat May 29 21:46:47 2021 -0500

    some big updates to how the pages are set up

commit 66611d5b2a0b338c11010a189a6e12264186e560
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Sat May 29 16:49:25 2021 -0500

    cleanup, better dev experience, got recursors to show

commit c9b4a30d05d5f3be9ed3754051d2a26b905f5940
Author: Adam Veldhousen <adam.veldhousen@liveauctioneers.com>
Date:   Sat May 29 15:33:02 2021 -0500

    checkpoint

commit f8dc94effcbab16f1811c7ed313beb9b2834d3ed
Author: Adam Veldhousen <adam.veldhousen@liveauctioneers.com>
Date:   Sat May 29 13:46:18 2021 -0500

    add env vars to globals

commit acd44f7e53b62e3c673800423300cfdcaada6ee9
Author: Adam Veldhousen <adam.veldhousen@liveauctioneers.com>
Date:   Sat May 29 13:39:25 2021 -0500

    update build for frontend

commit 9ed14e0574c335645883f8151919e488b36f5a5a
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Fri May 28 18:20:34 2021 -0500

    got go embed working with frontend

commit 56fe5d371fe6451e818ba65c2e2716b86c27eb91
Author: Adam Veldhousen <adamveld12@gmail.com>
Date:   Thu May 27 16:27:58 2021 -0500

    trying to add goembed
pull/1/head
Adam Veldhousen 3 years ago
parent b102e17f72
commit 8b76101107
Signed by: adam
GPG Key ID: 6DB29003C6DD1E4B

@ -1,26 +1,34 @@
# ADMIN DASHBOARD BUILD
FROM node:lts-alpine as build-client
COPY . /build/
WORKDIR /build/client
RUN apk add --no-cache make
RUN make build-client
COPY . /build/
RUN npm install && npm run build && mkdir -p /build/.bin/static && cp -R ./public /build/.bin/static
# DNS/API SERVER BUILD
FROM golang:alpine as build-server
COPY --from=build-client /build /build
WORKDIR /build/
RUN apk add --no-cache make
RUN apk add --no-cache --update make gcc musl-dev
COPY --from=build-client /build /build
RUN make .bin/gopherhole
# RUNTIME ENVIRONMENT
FROM alpine
RUN apl add --no-cache ca-certificates
WORKDIR /opt
RUN apk add --no-cache ca-certificates
RUN addgroup -g 1000 gopherhole \
&& adduser -H -D -u 1000 gopherhole gopherhole
RUN addgroup gopherhole \
&& adduser -H -D gopherhole gopherhole \
&& mkdir -p /data \
&& chown -R gopherhole /data
COPY --chown=gopherhole:gopherhole --from=build-server /build/.bin/gopherhole /opt/gopherhole

File diff suppressed because it is too large Load Diff

@ -1,42 +1,45 @@
{
"name": "svelte-app",
"version": "1.0.0",
"scripts": {
"build": "NODE_ENV=production rollup -c",
"dev": "rollup -c -w",
"start": "sirv public",
"validate": "svelte-check"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-typescript": "^8.1.0",
"@tsconfig/svelte": "^1.0.0",
"autoprefixer": "^10.2.3",
"postcss": "^8.2.4",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"svelte": "^3.38.2",
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.6.3",
"tailwindcss": "^2.0.2",
"tslib": "^2.0.0",
"typescript": "^4.1.3"
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"bootstrap": "^5.0.0",
"chart.js": "^3.2.1",
"date-fns": "^2.21.3",
"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"
}
"name": "svelte-app",
"version": "1.0.0",
"scripts": {
"build": "NODE_ENV=production rollup -c",
"dev": "NODE_ENV=development rollup -c -w",
"start": "sirv public --single -G -D",
"validate": "svelte-check"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.4.2",
"@rollup/plugin-typescript": "^8.1.0",
"@tsconfig/svelte": "^1.0.0",
"@types/node": "^15.6.1",
"autoprefixer": "^10.2.3",
"postcss": "^8.2.4",
"rollup": "^2.3.4",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"sirv-cli": "^1.0.0",
"svelte": "^3.38.2",
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.6.3",
"tailwindcss": "^2.0.2",
"tslib": "^2.0.0",
"typescript": "^4.1.3"
},
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"bootstrap": "^5.0.0",
"chart.js": "^3.2.1",
"date-fns": "^2.21.3",
"fa-svelte": "^3.1.0",
"normalize.css": "^8.0.1",
"randomcolor": "^0.6.2",
"svelte-awesome": "^2.3.1",
"svelte-chartjs": "^1.0.1",
"svelte-routing": "^1.6.0",
"sveltestrap": "^4.2.1"
}
}

@ -3,6 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="manifest" href="manifest.json">
<title>Gopherhole: A customizable DNS server</title>

@ -0,0 +1,47 @@
{
"name": "Gopherhole Dashboard",
"short_name": "Gopherhole",
"start_url": ".",
"display": "standalone",
"background_color": "white",
"theme_color": "rgb(55, 65, 81)",
"description": "A DNS server that help's you take back control of your network",
"icons": [
{
"src": "images/touch/homescreen48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "images/touch/homescreen72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "images/touch/homescreen96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "images/touch/homescreen144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "images/touch/homescreen168.png",
"sizes": "168x168",
"type": "image/png"
},
{
"src": "images/touch/homescreen192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"related_applications": [
{
"platform": "play",
"url": "https://play.google.com/store/apps/details?id=cheeaun.hackerweb"
}
]
}

@ -0,0 +1,6 @@
package public
import "embed"
//go:embed *
var Assets embed.FS

@ -6,69 +6,83 @@ import { terser } from "rollup-plugin-terser";
import sveltePreprocess from "svelte-preprocess";
import typescript from "@rollup/plugin-typescript";
import css from "rollup-plugin-css-only";
import replace from "@rollup/plugin-replace";
const production = !process.env.ROLLUP_WATCH;
const isDev = Boolean(process.env.ROLLUP_WATCH);
const production = !isDev;
function serve() {
let server;
let server;
function toExit() {
if (server) server.kill(0);
}
function toExit() {
console.log("~~~~ Shutting down dev server ~~~~");
return {
writeBundle() {
if (server) return;
server = require("child_process").spawn(
"npm",
["run", "start", "--", "--dev"],
{
stdio: ["ignore", "inherit", "inherit"],
shell: true,
}
);
if (server) server.kill(0);
}
process.on("SIGTERM", toExit);
process.on("exit", toExit);
},
};
return {
writeBundle() {
if (server) return;
server = require("child_process").spawn(
"npm",
["run", "start", "--", "--dev"],
{
stdio: ["ignore", "inherit", "inherit"],
shell: true,
}
);
process.on("SIGTERM", toExit);
process.on("exit", toExit);
},
};
}
export default {
input: "src/main.ts",
output: {
sourcemap: true,
format: "iife",
name: "app",
file: "public/build/bundle.js",
},
plugins: [
svelte({
// add postcss config with tailwind
preprocess: sveltePreprocess({
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
}),
compilerOptions: {
dev: !production,
},
}),
css({ output: "bundle.css" }),
resolve({
browser: true,
dedupe: ["svelte"],
}),
commonjs(),
typescript({
sourceMap: !production,
inlineSources: !production,
}),
!production && serve(),
!production && livereload("public"),
production && terser(),
],
watch: {
clearScreen: false,
},
input: "src/main.ts",
output: {
sourcemap: true,
format: "iife",
name: "app",
// dir: "public/build/",
file: "public/build/bundle.js",
},
plugins: [
replace({
preventAssignment: true,
include: ["src/**/*.ts", "src/**/*.svelte"],
values: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
"process.env.API_HOST": JSON.stringify(
isDev ? "http://localhost:8000" : ""
),
},
}),
svelte({
// add postcss config with tailwind
preprocess: sveltePreprocess({
postcss: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
}),
compilerOptions: { dev: isDev },
}),
css({ output: "bundle.css", sourceMap: true }),
resolve({
browser: true,
dedupe: ["svelte"],
}),
commonjs(),
typescript({
sourceMap: true,
inlineSources: isDev,
cacheDir: "node_modules/.tmp/.rollup.tscache",
}),
isDev && serve(),
isDev && livereload({ watch: "public", delay: 200 }),
production && terser(),
],
watch: {
clearScreen: false,
},
};

@ -10,6 +10,7 @@
faCloudUploadAlt,
faTrafficLight,
} from "@fortawesome/free-solid-svg-icons";
import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
import Home from "./routes/Home.svelte";

@ -0,0 +1,94 @@
import { apiCall } from './util'
import { getUnixTime, sub } from 'date-fns';
export interface Recursor {
ipAddress: string
timeoutMs: number
weight: number
}
export const getRecursors = async () => await apiCall<Recursor>('recursors')
export interface Log {
Started: string
Domain: string
ClientIP: string
Status: string
Protocol: string
Error: string
RecurseUpstreamIP: string
RecurseRoundTripTimeMs: number
TotalTimeMs: number
};
export interface LogSearchOptions {
start: Date
end: Date
page: number
filter: string
}
export const getLogs = async({
start = sub(new Date(), { hours: 24 }),
end = new Date(),
page = 0,
filter = ""
}: LogSearchOptions) => await apiCall<Log>('metrics/log', 'GET', {
filter,
page,
start: getUnixTime(start),
end: getUnixTime(end),
});
export interface StatsSearchOptions {
start: Date
end: Date
key: StatSearchKey
interval: number
}
export enum StatSearchKey {
Domain = "domain",
ClientIp = "clientIp",
Status = "status",
Protocol = "protocol"
}
export interface Stat {
Header: string
AverageTotalTime: Number
Count: number,
Time: string
};
export const getStats = async ({
start = sub(new Date(), { hours: 24 }),
end = new Date(),
key = StatSearchKey.Domain,
interval = 30,
}: StatsSearchOptions) => await apiCall<Stat>('metrics/stats', 'GET', {
start: getUnixTime(start),
end: getUnixTime(end),
key,
interval
});
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');

@ -1,26 +0,0 @@
import { sub, getUnixTime } from 'date-fns'
import { API_HOST } from './util';
export const getLogs = async function ({
start = sub(new Date(), { hours: 12 }),
end = new Date(),
page = 0,
filter = ""
} = {}) {
try {
const data = await fetch(API_HOST(`metrics/log?filter=${filter}&start=${getUnixTime(start)}&end=${getUnixTime(end)}&page=${page}`), {
"method": "GET",
"headers": { "Accept": "application/json" }
});
const { success, payload } = await data.json();
if (success) {
return { payload, start, end, filter };
}
return { error: payload };
} catch (error) {
console.error(error);
return { error };
}
}

@ -1,64 +0,0 @@
import { getUnixTime, sub, format } from 'date-fns';
import { readable } from "svelte/store";
import { API_HOST } from './util';
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`
`(prop:expr)*
*/
export const parseTerms = (terms: string = "") :[string] | [] => {
const matches = terms.match(/([a-z]+[:>][\w\.]+)*/ig);
const exprs = matches.reduce((agg, term) => {
}, []);
return [];
};
export const fetchMetrics = async function ({
start = sub(new Date(), { hours: 8 }),
end = new Date(),
key = "domain",
interval = "30",
} = {}) {
try {
const response = await fetch(
API_HOST(`metrics/stats?start=${getUnixTime(
start
)}&end=${getUnixTime(end)}&key=${key}&interval=${interval}`),
{
method: "GET",
headers: { Accept: "application/json" }
}
);
const { success, payload } = await response.json();
if (!success) {
return { error: payload };
}
return { payload, interval, start, end, key };
} catch (e) {
console.error(e);
return { error: e };
}
};
export const metricsStore = readable([], (set) => {
const interval = setInterval(async () => {
const { error, payload } = await fetchMetrics({});
if (error) {
console.error(error);
return;
}
console.log(payload);
set(payload);
}, 15 * 1000);
return () => clearInterval(interval);
});

@ -1,9 +0,0 @@
import { apiCall } from './util'
export interface Recursor {
ipAddress: string
timeoutMs: number
weight: number
}
export const getRecursors = () => apiCall<Recursor>('recursors')

@ -1,19 +0,0 @@
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');

@ -1,3 +1,5 @@
import { sub, format, fromUnixTime } from 'date-fns';
interface APIResponse<T> {
success?: boolean
payload?: T[]
@ -5,11 +7,28 @@ interface APIResponse<T> {
}
export const API_HOST = (url = "") => `http://localhost:8000/api/v1/${url}`;
const api_host = process.env.API_HOST;
export const fromUnixTimeSafe = (value: string = ""): Date => {
const n = Number(value);
if (!n || isNaN(n)) {
return null;
}
return fromUnixTime(n);
};
export const apiCall = async function<T>(url: string, method: string = 'GET'): Promise<APIResponse<T>> {
export const createSearchDate = (start: Date = new Date(), hours: number = 0): Date => {
const d = sub(start, { hours });
d.setSeconds(0);
return d;
}
export const buildQueryParams = (qps = null) => qps == null ? "" : `?${Object.keys(qps).filter(key => !!qps[key] && (qps[key] !== "undefined")).map(key => `${key}=${qps[key]}`).join('&')}`
export const API_HOST = (url = "", queryParams) => `${api_host}/api/v1/${url}${buildQueryParams(queryParams)}`;
export const apiCall = async function <T>(url: string, method: string = 'GET', queryParams = null): Promise<APIResponse<T>> {
try {
const data = await fetch(`${API_HOST}/${url}`, {
const data = await fetch(API_HOST(url, queryParams), {
headers: { "Accept": "application/json" },
method,
});
@ -27,3 +46,7 @@ export const apiCall = async function<T>(url: string, method: string = 'GET'): P
return { error };
}
}
export const SearchDateFormatStr = "yyyy-MM-dd HH:mm:ss";
export const formatDateForSearch = date => format(date, SearchDateFormatStr);

@ -1,5 +1,5 @@
<script lang="ts">
import { format, parse } from "date-fns";
import { format, fromUnixTime, getUnixTime, parse } from "date-fns";
import {
Input,
@ -7,16 +7,13 @@
InputGroupAddon,
InputGroupText,
} from "sveltestrap";
import { SearchDateFormatStr } from "../api/metrics";
import { SearchDateFormatStr } from "../api/util";
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());
@ -25,19 +22,24 @@
time: format(dt, "HH:mm:ss"),
});
const isDate = (v) => (v.match(/[\d]{4}-\d{2}-\d{2}/) || []).length > 0;
const isDate = (v: string) =>
(v.match(/[\d]{4}-\d{2}-\d{2}/) || []).length > 0;
let dateTimeParts = toDateTimeParts(defaultValue);
export let label: string = "label";
export let defaultValue: Date = new Date();
export let value: Date = defaultValue;
$: dateTimeParts = toDateTimeParts(value);
const update = ({ target: { value } }) => {
const update = ({ target: { value: v } }) => {
const { date, time } = dateTimeParts;
let dateTimePartsInput = isDate(value)
? { date: value, time }
: { date, time: value };
const updatedDateTime = fromDateTimeParts(dateTimePartsInput);
dateTimeParts = toDateTimeParts(updatedDateTime);
console.log(dateTimeParts);
let dateTimePartsInput = isDate(v)
? { date: v, time }
: { date, time: v };
value = fromDateTimeParts(dateTimePartsInput);
// dateTimeParts = toDateTimeParts(value);
};
</script>

@ -1,22 +1,14 @@
<script lang="ts">
import { onMount } from "svelte";
import { Column, Table } from "sveltestrap";
import { getLogs } from "../api/logs";
import type { Log } from "../api";
let rows = [];
onMount(async () => {
try {
const { payload } = await getLogs();
rows = payload || [];
} catch (error) {
console.error(error);
}
});
// export let page: number = 0;
export let logs: Log[] = [];
</script>
<div class="flex flex-column text-sm">
{#if rows.length > 0}
<Table {rows} let:row hover bordered>
{#if logs && logs.length > 0}
<Table rows={logs} let:row hover bordered>
<Column header="Started">
{row.Started}
</Column>
@ -49,6 +41,8 @@
</Table>
{:else}
<p>No Logs yet!</p>
<p><em>TODO:</em> Link to docs on how to point your router at this server</p>
<p>
<em>TODO:</em> Link to docs on how to point your router at this server
</p>
{/if}
</div>

@ -1,8 +1,8 @@
<script lang="ts">
import { onMount } from "svelte";
import { Column, Row, Table } from "sveltestrap";
import { getRules } from "../api/rules";
import type { Rule } from "../api/rules";
import { getRules } from "../api";
import type { Rule } from "../api";
let rows: Rule[] = [];
onMount(async () => {

@ -1,51 +1,42 @@
<script lang="ts">
import sub from "date-fns/sub";
import {
Dropdown,
DropdownMenu,
DropdownItem,
DropdownToggle,
Form,
FormGroup,
InputGroup,
InputGroupAddon,
InputGroupText,
Label,
Input,
} from "sveltestrap";
import { StatSearchKey } from "../api";
import DatetimePicker from "./DatetimePicker.svelte";
export let start: Date = sub(new Date(), { hours: 24 });
export let end: Date = new Date();
export let key: StatSearchKey = StatSearchKey.Domain;
export let filter: string = "";
$: rawFilter = filter;
let menuItem = [
{ label: "Domain Name", key: "domain" },
{ label: "Client IP", key: "clientIp" },
{ label: "Protocol", key: "protocol" },
{ label: "Status", key: "status" },
{ label: "Domain Name", key: StatSearchKey.Domain },
{ label: "Client IP", key: StatSearchKey.ClientIp },
{ label: "Protocol", key: StatSearchKey.Protocol },
{ label: "Status", key: StatSearchKey.Status },
];
let selected = menuItem[0];
const selectItem = (selectedItem) => {
selected = selectedItem;
handleOnChange();
};
let searchFilter = "";
let startDate = new Date();
let endDate = new Date();
$: selected = !!key ? menuItem.find((v) => v.key === key) : menuItem[0];
const handleOnChange = () => {
if (onChange) {
const event = {
terms: searchFilter,
start: startDate,
end: endDate,
key: selected.key,
};
const selectItem = (selectedItem) => (key = selectedItem.key);
onChange(event);
}
const onFilterChanged = ({ target: { value } }) => {
console.log(value);
filter = value;
};
export let onChange = (evt) =>
console.log("no handler bound to SearchOptions.svelte: ", evt);
</script>
<div class="w-full flex flex-row">
@ -55,17 +46,16 @@
<InputGroupAddon addonType="prepend">
<InputGroupText>Search</InputGroupText>
</InputGroupAddon>
<Input
id="search"
type="search"
on:change={handleOnChange}
bind:value={searchFilter}
/>
<Input id="search" type="search" on:change={onFilterChanged} />
</InputGroup>
</FormGroup>
<FormGroup class="flex flex-row">
<DatetimePicker label="Start" />
<DatetimePicker label="End" />
<DatetimePicker
label="Start"
defaultValue={start}
bind:value={start}
/>
<DatetimePicker label="End" defaultValue={end} bind:value={end} />
</FormGroup>
<FormGroup class="mx-2">
<Dropdown>

@ -1,45 +1,22 @@
<script>
import { fetchMetrics } from "../api/metrics.ts";
<script lang="ts">
import { onMount } from "svelte";
import type { Stat } from "../api";
import randomColor from "randomcolor";
import { Chart, registerables } from "chart.js";
Chart.register(...registerables);
let canvas;
onMount(async () => {
const ctx = canvas.getContext("2d");
const { labels, datasets } = await fetchAndPrepareMetrics();
var c = new Chart(ctx, {
type: "line",
data: { labels, datasets },
options: {
responsive: true,
maintainAspectRatio: false,
title: {
display: true,
text: `Top Requests`,
},
scales: {
y: {
stacked: true,
},
},
},
});
});
const fetchAndPrepareMetrics = async () => {
const { error, payload } = await fetchMetrics({ key: "clientIp" });
if (error) {
return {};
}
export let stats: Stat[] = [];
const chartData = payload.reduce((agg, x) => {
const transformStats = (ostats) => {
const chartData = ostats.reduce((agg, x) => {
let root = agg[x.Header] || {
labels: [],
dataset: {
label: x.Header,
borderColor: "rgb(75,192,192)",
borderColor: randomColor({
luminosity: "dark",
}), //"rgb(75,192,192)",
data: [],
},
};
@ -50,15 +27,108 @@
}, {});
const finalChartData = Object.keys(chartData).map((x) => chartData[x]);
const finalChartLabels = finalChartData[0].labels;
const finalChartLabels =
finalChartData.length > 0 ? finalChartData[0].labels : [];
return {
labels: finalChartLabels,
datasets: finalChartData.map((x) => x.dataset),
};
};
const generateChartOptions = (s: [], empty: Boolean = false) => {
let labels = [];
let datasets = [];
if (s && s.length > 0) {
({ labels, datasets } = transformStats(s));
}
var delayed;
return {
type: "line",
data: {
labels,
datasets,
},
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
// x: {
// type: "time",
// ticks: {
// source: "auto",
// // Disabled rotation for performance
// maxRotation: 0,
// autoSkip: true,
// },
// },
y: {
stacked: true,
},
},
hoverRadius: 5,
interaction: {
mode: "nearest",
intersect: false,
axis: "x",
},
plugins: {
decimation: {
enabled: true,
algorithm: "lttb",
samples: 60,
},
},
animations: {
radius: {
duration: 150,
easing: "linear",
loop: (c) => c.active,
},
},
// animation: {
// onComplete: () => {
// delayed = true;
// },
// delay: (context) => {
// let delay = 0;
// if (
// context.type === "data" &&
// context.mode === "default" &&
// !delayed
// ) {
// delay =
// context.dataIndex * 30 +
// context.datasetIndex * 10;
// }
// return delay;
// },
// },
},
};
};
let canvas = null;
let chartInstance = null;
onMount(async () => {
const ctx = canvas.getContext("2d");
chartInstance = new Chart(ctx, generateChartOptions(stats, true));
});
const update = (s) => {
if (chartInstance) {
const { options, data } = generateChartOptions(s, false);
chartInstance.options = options;
chartInstance.data = data;
chartInstance.update();
}
};
$: update(stats);
</script>
<div style="position: relative; max-height: 512px; height: 512px; width: 100%;">
<div style="position: relative; max-height: 384px; height: 384px; width: 100%;">
<canvas width="100%" height="100%" bind:this={canvas} />
</div>

@ -4,4 +4,5 @@ const app = new App({
target: document.body,
});
export default app;

@ -1,19 +1,128 @@
<script lang="ts">
import LogViewer from "../components/LogViewer.svelte";
import { onMount } from "svelte";
import { navigate } from "svelte-routing";
import { getUnixTime, isEqual, sub } from "date-fns";
import { buildQueryParams, fromUnixTimeSafe } from "../api/util";
import { getLogs, getStats, StatSearchKey } from "../api";
import type { Stat, Log } from "../api";
import PageContainer from "./PageContainer.svelte";
import SearchOptions from "../components/SearchOptions.svelte";
import TimeChart from "../components/TimeChart.svelte";
import PageContainer from "./PageContainer.svelte";
import LogViewer from "../components/LogViewer.svelte";
export let location: Location;
const { search } = location;
let params = new URLSearchParams(search.substring(1));
export let start: Date =
fromUnixTimeSafe(params.get("start")) || sub(new Date(), { hours: 24 });
export let end: Date = fromUnixTimeSafe(params.get("end")) || new Date();
export let filter: string = params.get("filter") || "";
export let chartKey: StatSearchKey =
StatSearchKey[params.get("key")] || StatSearchKey.Domain;
export let chartInterval: number = 30;
export let logPage: number = 0;
let logErrorMsg: string = null;
let chartErrorMsg: string = null;
let chartDataLoading: Boolean = true;
let logDataLoading: Boolean = true;
let chartData: Stat[] = [];
let logData: Log[] = [];
const fetchLogs = async () => {
if (logDataLoading) {
console.warn("tried loading logs while already loading");
}
logErrorMsg = null;
logDataLoading = true;
const { error, payload } = await getLogs({
start,
end,
page: logPage,
filter,
});
logDataLoading = false;
if (error) {
logErrorMsg = error;
return [];
}
return payload;
};
const fetchStats = async () => {
if (chartDataLoading) {
console.warn("tried loading stats while already loading");
}
chartErrorMsg = null;
chartDataLoading = true;
const { error, payload } = await getStats({
start,
end,
key: chartKey,
interval: chartInterval,
});
chartDataLoading = false;
if (error) {
chartErrorMsg = error;
return [];
}
return payload;
};
const updateData = async (evt) => {
console.groupCollapsed("Stats Data Update");
const { filter: eFilter, start: eStart, end: eEnd, key: eKey } = evt;
navigate(
`${location?.pathname}${buildQueryParams({
start: getUnixTime(eStart),
end: getUnixTime(eEnd),
filter: eFilter,
key: eKey,
})}`,
{ replace: true }
);
[logData, chartData] = await Promise.all([fetchLogs(), fetchStats()]);
console.info("handled search, fetching new data:", evt);
console.groupEnd();
};
$: updateData({ start, end, key: chartKey, filter });
</script>
<PageContainer
{location}
header="Stats"
description="Server performance stats and activity log"
>
<SearchOptions />
<section class="my-5">
<TimeChart />
<SearchOptions bind:start bind:end bind:filter bind:key={chartKey} />
<section style="height: 384px;" class="my-5">
{#if chartDataLoading}
<p>Loading chart</p>
{:else if chartErrorMsg}
<p>{chartErrorMsg}</p>
{:else}
<TimeChart stats={chartData} />
{/if}
</section>
<section class="my-5">
<LogViewer />
{#if logDataLoading}
<p>Loading logs</p>
{:else if logErrorMsg}
<p>{logErrorMsg}</p>
{:else}
<LogViewer logs={logData} />
{/if}
</section>
</PageContainer>

@ -1,7 +1,12 @@
<script lang="ts">
import { onMount } from "svelte";
import { fade, fly } from "svelte/transition";
export let header: string = "Home";
export let description: string = "";
export let location: Location;
onMount(() => console.log(`Page mounted ${location.pathname}`));
</script>
<section out:fade={{ duration: 100 }} class="w-full">

@ -3,8 +3,8 @@
import { onMount } from "svelte";
import { Column, Row, Table } from "sveltestrap";
import { getRecursors } from "../api/recursors";
import type { Recursor } from "../api/recursors";
import { getRecursors } from "../api";
import type { Recursor } from "../api";
let rows: Recursor[] = [];
onMount(async () => {

@ -1,6 +1,6 @@
{
"extends": "@tsconfig/svelte/tsconfig.json",
"types": ["node"],
"include": ["src/**/*"],
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
}
}

@ -1,6 +1,6 @@
module github.com/adamveld12/gopherhole
go 1.15
go 1.16
require (
github.com/go-chi/chi v1.5.4

Binary file not shown.

@ -1,8 +1,10 @@
package internal
import (
"io/fs"
"log"
"net/http"
"os"
"runtime"
"time"
@ -26,7 +28,7 @@ type adminHandler struct {
h http.Handler
}
func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
func NewAdminHandler(c Cache, s Storage, re *RuleEngine, content fs.FS) http.Handler {
handler := chi.NewRouter()
a := &adminHandler{
@ -39,20 +41,34 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
handler.Use(middleware.RequestID)
handler.Use(middleware.RealIP)
handler.Use(middleware.Logger)
handler.Use(middleware.AllowContentType("application/json; utf-8", "application/json"))
handler.Use(middleware.Recoverer)
handler.Use(middleware.Timeout(time.Second * 5))
// TODO: smarter way https://github.com/go-chi/chi/issues/403
handler.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://*", "https://*"},
AllowedMethods: []string{"GET", "PUT", "DELETE", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Accept"},
AllowCredentials: false,
MaxAge: 300,
}))
// handler.Handle("/build/", http.StripPrefix("/build/", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
// file, err := content.Open(fmt.Sprintf("client/public/build/%s", r.URL.Path))
// if err != nil {
// rw.WriteHeader(http.StatusNotFound)
// return
// }
// defer file.Close()
handler.Use(middleware.Recoverer)
// if _, err := io.Copy(rw, file); err != nil {
// rw.WriteHeader(http.StatusInternalServerError)
// return
// }
// })))
handler.Route("/api/v1", func(r chi.Router) {
r.Use(middleware.AllowContentType("application/json; utf-8", "application/json"))
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://*", "https://*"},
AllowedMethods: []string{"GET", "PUT", "DELETE", "POST", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Accept"},
AllowCredentials: false,
MaxAge: 300,
}))
r.Get("/metrics/log", RestHandler(a.getLog).ToHF())
r.Get("/metrics/stats", RestHandler(a.getStats).ToHF())
@ -79,6 +95,23 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
r.HandleFunc("/signal", a.signal)
})
fs := http.FS(content)
httpFileServer := http.FileServer(fs)
handler.Handle("/*", http.StripPrefix("/", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
f, err := content.Open(r.URL.Path)
if os.IsNotExist(err) {
r.URL.Path = "/"
}
if err == nil {
f.Close()
}
log.Printf("%s - err: %v", r.URL.Path, err)
httpFileServer.ServeHTTP(rw, r)
})))
return a
}

@ -51,7 +51,7 @@ func (ss *Sqlite) GetRecursors() ([]RecursorRow, error) {
defer rows.Close()
var results []RecursorRow
results := []RecursorRow{}
for rows.Next() {
var row RecursorRow

@ -10,6 +10,7 @@ import (
"strings"
"time"
"github.com/adamveld12/gopherhole/client/public"
"github.com/adamveld12/gopherhole/internal"
"github.com/miekg/dns"
)
@ -19,6 +20,7 @@ var (
// dbPath = flag.String("db-path", ".", "Directory to write database files to")
// httpAddr = flag.String("http-address", ":8080", "Bind address for http server")
// dnsAddr = flag.String("dns-address", ":53", "Bind address for dns server")
)
func main() {
@ -69,7 +71,7 @@ func main() {
}
}()
httpApi := internal.NewAdminHandler(cache, store, re)
httpApi := internal.NewAdminHandler(cache, store, re, public.Assets)
if err := http.ListenAndServe(conf.HTTPAddr, httpApi); err != nil {
log.Fatal(err)
}

@ -1,17 +1,16 @@
build: clobber .bin/client/public .bin/gopherhole
dev: clean .bin/gopherhole .bin/config.json
cd .bin && ./gopherhole -config config.json
client-dev: client/node_modules
cd ./client && npm run dev
client/node_modules:
cd ./client && npm install
clean:
@rm -rf .bin/gopherhole .bin/config.json
@rm -rf .bin/gopherhole .bin/config.json .bin/client
clobber:
@rm -rf .bin ./client/node_modules
clobber: clean
@rm -rf .bin ./client/node_modules ./client/public/build
vdhsn/gopherhole:
docker build -t vdhsn/gopherhole:latest .
test:
dig -p 5353 twitter.com @localhost
@ -19,14 +18,28 @@ test:
dig -p 5353 loki.veldhousen.ninja @localhost
dig -p 5353 www.liveauctioneers.com @localhost
.PHONY: clean clobber dev
.PHONY: build clean clobber client-dev dev test vdhsn/gopherhole
.bin:
mkdir -p .bin
.bin/gopherhole: .bin
# @go build --tags "sqlite_foreign_keys fts5" -v -o .bin/gopherhole .
# @go build --tags "sqlite_foreign_keys fts5" -v -o .bin/gopherhole .
@go build --tags "fts5" -v -o .bin/gopherhole .
.bin/config.json:
@cp ./config.example.json .bin/config.json
client-dev: client/node_modules
cd ./client && npm run dev
.bin/client/public: .bin client/public/build
mkdir -p .bin/client/public
cp -R ./client/public/ .bin/client/
client/public/build: client/node_modules
cd ./client && npm run build
client/node_modules:
cd ./client && npm install

@ -0,0 +1,10 @@
#!/bin/bash
while :
do
dig -p 5353 twitter.com @localhost
dig -p 5353 google.com @localhost
dig -p 5353 loki.veldhousen.ninja @localhost
dig -p 5353 www.liveauctioneers.com @localhost
sleep 1;
done
Loading…
Cancel
Save