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 @@ + + +
+
+ + + + Search + + + + + + + + + + + By {selected.label} + + {#each menuItem as item} + + selectItem(item)}> + {item.label} + + + {/each} + + + +
+
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 @@ + + +
+
+

Home

+
+
+ +
+
+ +
+
+ +
+
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