diff --git a/.gitignore b/.gitignore
index 3657509..e22d33e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,7 @@
.bin
-.idea
\ No newline at end of file
+.idea
+
+client/node_modules/
+client/public/build/
+
+.DS_Store
diff --git a/client/.gitignore b/client/.gitignore
new file mode 100644
index 0000000..b679f81
--- /dev/null
+++ b/client/.gitignore
@@ -0,0 +1,6 @@
+/node_modules/
+/public/build/
+.vscode/
+.DS_Store
+package-lock.json
+yarn.lock
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 0000000..6bc6192
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,10 @@
+# Svelte TypeScript Tailwindcss Setup
+
+svelte template based on the default svelte/template with enabled typescript and tailwindcss support.
+
+```bash
+npx degit munxar/svelte-template my-svelte-project
+cd my-svelte-project
+npm i
+npm run dev
+```
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 0000000..0e170a2
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,41 @@
+{
+ "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-chartjs": "^1.0.1",
+ "svelte-routing": "^1.6.0",
+ "sveltestrap": "^4.2.1"
+ }
+}
diff --git a/client/public/favicon.png b/client/public/favicon.png
new file mode 100644
index 0000000..7e6f5eb
Binary files /dev/null and b/client/public/favicon.png differ
diff --git a/client/public/index.html b/client/public/index.html
new file mode 100644
index 0000000..4af38ab
--- /dev/null
+++ b/client/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Svelte app
+
+
+
+
+
+
+
+
+
diff --git a/client/rollup.config.js b/client/rollup.config.js
new file mode 100644
index 0000000..8bb6116
--- /dev/null
+++ b/client/rollup.config.js
@@ -0,0 +1,74 @@
+import svelte from "rollup-plugin-svelte";
+import commonjs from "@rollup/plugin-commonjs";
+import resolve from "@rollup/plugin-node-resolve";
+import livereload from "rollup-plugin-livereload";
+import { terser } from "rollup-plugin-terser";
+import sveltePreprocess from "svelte-preprocess";
+import typescript from "@rollup/plugin-typescript";
+import css from "rollup-plugin-css-only";
+
+const production = !process.env.ROLLUP_WATCH;
+
+function serve() {
+ let server;
+
+ function toExit() {
+ if (server) server.kill(0);
+ }
+
+ 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,
+ },
+};
diff --git a/client/src/App.svelte b/client/src/App.svelte
new file mode 100644
index 0000000..650db01
--- /dev/null
+++ b/client/src/App.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/Tailwind.svelte b/client/src/Tailwind.svelte
new file mode 100644
index 0000000..b45f6e8
--- /dev/null
+++ b/client/src/Tailwind.svelte
@@ -0,0 +1,14 @@
+
diff --git a/client/src/api/logs.ts b/client/src/api/logs.ts
new file mode 100644
index 0000000..fc9f598
--- /dev/null
+++ b/client/src/api/logs.ts
@@ -0,0 +1,25 @@
+import { sub } from 'date-fns'
+import { formatDate } from './metrics';
+
+
+export const getLogs = async function ({
+ start = sub(new Date(), { hours: 12 }),
+ end = new Date(),
+ filter = ""
+} = {}) {
+ try {
+ const data = await fetch(`http://localhost:8080/api/v1/metrics/log?filter=${filter}`, {
+ "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 };
+ }
+}
diff --git a/client/src/api/metrics.ts b/client/src/api/metrics.ts
new file mode 100644
index 0000000..42a1726
--- /dev/null
+++ b/client/src/api/metrics.ts
@@ -0,0 +1,62 @@
+import { sub, format } from 'date-fns';
+import { readable } from "svelte/store";
+
+export const formatDate = date => format(date, "yyyy-MM-dd HH:mm:ss.SSS");
+
+/*
+ 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(
+ `http://localhost:8080/api/v1/metrics/stats?start=${formatDate(
+ start
+ )}&end=${formatDate(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);
+ });
diff --git a/client/src/components/DatetimePicker.svelte b/client/src/components/DatetimePicker.svelte
new file mode 100644
index 0000000..c54593b
--- /dev/null
+++ b/client/src/components/DatetimePicker.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+ {label}
+
+
+
+
diff --git a/client/src/components/LogViewer.svelte b/client/src/components/LogViewer.svelte
new file mode 100644
index 0000000..7d13498
--- /dev/null
+++ b/client/src/components/LogViewer.svelte
@@ -0,0 +1,52 @@
+
+
+
+ {#if rows.length > 0}
+
+
+ {row.Started}
+
+
+ {row.Domain}
+
+
+ {row.ClientIP}
+
+
+ {row.Status}
+
+
+ {row.Protocol}
+
+
+ {#if row.Error}
+ {row.Error}
+ {/if}
+
+
+ {row.RecurseRoundTripTimeMs}
+
+
+ {row.RecurseUpstreamIP}
+
+
+ {row.TotalTimeMs}
+
+
+ {/if}
+
diff --git a/client/src/components/SearchOptions.svelte b/client/src/components/SearchOptions.svelte
new file mode 100644
index 0000000..2782e8f
--- /dev/null
+++ b/client/src/components/SearchOptions.svelte
@@ -0,0 +1,85 @@
+
+
+
+
+
diff --git a/client/src/components/TimeChart.svelte b/client/src/components/TimeChart.svelte
new file mode 100644
index 0000000..cedac97
--- /dev/null
+++ b/client/src/components/TimeChart.svelte
@@ -0,0 +1,66 @@
+
+
+
+
+
diff --git a/client/src/main.ts b/client/src/main.ts
new file mode 100644
index 0000000..83bc6ab
--- /dev/null
+++ b/client/src/main.ts
@@ -0,0 +1,7 @@
+import App from "./App.svelte";
+
+const app = new App({
+ target: document.body,
+});
+
+export default app;
diff --git a/client/src/routes/Home.svelte b/client/src/routes/Home.svelte
new file mode 100644
index 0000000..c341567
--- /dev/null
+++ b/client/src/routes/Home.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/client/tailwind.config.js b/client/tailwind.config.js
new file mode 100644
index 0000000..a5386f1
--- /dev/null
+++ b/client/tailwind.config.js
@@ -0,0 +1,24 @@
+const { tailwindExtractor } = require("tailwindcss/lib/lib/purgeUnusedStyles");
+
+module.exports = {
+ purge: {
+ content: ["src/**/*.svelte", "public/index.html"],
+ options: {
+ defaultExtractor: (content) => [
+ ...tailwindExtractor(content),
+ ...[...content.matchAll(/(?:class:)*([\w\d-/:%.]+)/gm)].map(
+ ([_match, group, ..._rest]) => group
+ ),
+ ],
+ keyframes: true,
+ },
+ },
+ darkMode: false, // or 'media' or 'class'
+ theme: {
+ extend: {},
+ },
+ variants: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/client/tsconfig.json b/client/tsconfig.json
new file mode 100644
index 0000000..b082e96
--- /dev/null
+++ b/client/tsconfig.json
@@ -0,0 +1,6 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+
+ "include": ["src/**/*"],
+ "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
+}
\ No newline at end of file
diff --git a/internal/dnsHandler.go b/internal/dnsHandler.go
index 52f79ee..2c3b61e 100644
--- a/internal/dnsHandler.go
+++ b/internal/dnsHandler.go
@@ -29,6 +29,7 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
}
var resolved Resolved
+ var err error
// lookup in cache
if dest := dm.LookupRecord(q.Name); dest != nil {
responseMessage = new(dns.Msg)
@@ -39,7 +40,7 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
responseMessage = rule.CreateAnswer(q.Name)
responseMessage.Authoritative = true
ql.Status = CustomRule
- } else if resolved, ql.Error = dm.Recursors.Resolve(r); ql.Error == nil {
+ } else if resolved, err = dm.Recursors.Resolve(r); err == nil {
dm.SaveAnswers(q.Name, resolved.Message.Answer)
responseMessage = resolved.Message
ql.RecurseUpstreamIP = resolved.UpstreamUsed
@@ -47,6 +48,10 @@ func (dm *DomainManager) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
ql.Status = RecursedUpstream
}
+ if err != nil {
+ ql.Error = err.Error()
+ }
+
responseMessage.SetReply(r)
responseMessage.RecursionAvailable = true
responseMessage.Compress = true
@@ -82,6 +87,6 @@ type QueryLog struct {
TotalTimeMs int
RecurseRoundTripTimeMs int
RecurseUpstreamIP string
- Error error
+ Error string
Status ResponseStatus
}
diff --git a/internal/sqlite.go b/internal/sqlite.go
index 1a46c29..719e096 100644
--- a/internal/sqlite.go
+++ b/internal/sqlite.go
@@ -2,7 +2,6 @@ package internal
import (
"database/sql"
- "errors"
"fmt"
"io"
"net"
@@ -103,7 +102,7 @@ type RuleRow struct {
}
func (ss *Sqlite) AddRule(rr RuleRow) error {
- sql := `INSERT INTO rules (name, expression, answerType, answerValue, ttl, weight, enabled, created)
+ sql := `INSERT INTO rules (name, expression, answerType, answerValue, ttl, weight, enabled, created)
VALUES (?, ? , ?, ?, ?, ?, 1, ?);`
if _, err := ss.Exec(sql, rr.Name, rr.Value, rr.Answer.Type, rr.Answer.Value, rr.TTL, rr.Weight, time.Now().UTC().Format(ISO8601)); err != nil {
@@ -194,12 +193,12 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
}
sql := `
- SELECT
+ SELECT
started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status
FROM
log
WHERE
- id > ? AND started > ? AND started < ?
+ id > ? AND started > ? AND started < ?
ORDER BY started DESC
LIMIT ?;
`
@@ -213,7 +212,6 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
var ql []QueryLog
for rows.Next() {
var q QueryLog
- var errStr string
var started string
rows.Scan(
@@ -222,16 +220,12 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
&q.Protocol,
&q.Domain,
&q.TotalTimeMs,
- &errStr,
+ &q.Error,
&q.RecurseRoundTripTimeMs,
&q.RecurseUpstreamIP,
&q.Status,
)
- if errStr != "" {
- q.Error = errors.New(errStr)
- }
-
if q.Started, err = time.Parse(ISO8601, started); err != nil {
return nil, fmt.Errorf("could not parse time '%s': %v", started, err)
}
@@ -294,7 +288,7 @@ func (ss *Sqlite) GetLogAggregate(la LogAggregateInput) ([]LogAggregateDataPoint
strftime('%%s', started)/(%d) as "timeWindow"
FROM log
GROUP BY %s, strftime('%%s', started) / (%d)
- ORDER BY started desc;
+ ORDER BY started ASC;
`
sql = fmt.Sprintf(sql, column, timeWindow, column, timeWindow)
@@ -328,22 +322,18 @@ func (ss *Sqlite) GetLogAggregate(la LogAggregateInput) ([]LogAggregateDataPoint
func (ss *Sqlite) Log(ql QueryLog) error {
sql := `
- INSERT INTO log
+ INSERT INTO log
(started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?);
`
- var errStr string
- if ql.Error != nil {
- errStr = ql.Error.Error()
- }
if _, err := ss.DB.Exec(sql,
ql.Started.Format(ISO8601),
ql.ClientIP,
ql.Protocol,
ql.Domain,
ql.TotalTimeMs,
- errStr,
+ ql.Error,
ql.RecurseRoundTripTimeMs,
ql.RecurseUpstreamIP,
ql.Status,
@@ -386,6 +376,7 @@ func initTable(db *sql.DB) error {
status TEXT NOT NULL
);
+
CREATE TABLE IF NOT EXISTS rules (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
@@ -400,6 +391,7 @@ func initTable(db *sql.DB) error {
CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_name ON rules (name);
CREATE UNIQUE INDEX IF NOT EXISTS idx_rules_expression ON rules (expression);
+
CREATE TABLE IF NOT EXISTS recursors (
id INTEGER PRIMARY KEY,
ipAddress TEXT NOT NULL,
@@ -407,6 +399,27 @@ func initTable(db *sql.DB) error {
weight INT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_recursors_ipAddress ON recursors (ipAddress);
+
+
+ CREATE TABLE IF NOT EXISTS ruleslist (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ url TEXT NOT NULL,
+ lastLoadedTs TEXT NOT NULL,
+ weight INT NOT NULL
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_name ON ruleslist (name);
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_url ON ruleslist (url);
+
+
+ CREATE TABLE IF NOT EXISTS ruleslist_entry (
+ id INTEGER PRIMARY KEY,
+ ruleslistId INTEGER NOT NULL,
+ expression TEXT NOT NULL,
+ ipAddress TEXT NOT NULL
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_entry_expression ON ruleslist_entry (expression);
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_ruleslist_entry_ruleslistId ON ruleslist_entry (id, ruleslistId);
`
if _, err := db.Exec(sql); err != nil {
diff --git a/makefile b/makefile
index c888a6e..57a9a9f 100644
--- a/makefile
+++ b/makefile
@@ -1,11 +1,17 @@
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
clobber:
- @rm -rf .bin
+ @rm -rf .bin ./client/node_modules
test:
dig -p 5353 twitter.com @localhost