Compare commits

...

2 Commits

Author SHA1 Message Date
Adam Veldhousen 9c147d1b79
fixed some time related bugs with metrics/stats queries 2021-05-27 03:20:25 -05:00
Adam Veldhousen 1c43e4479a
refactoring 2021-05-27 02:07:09 -05:00
17 changed files with 2706 additions and 348 deletions

View File

@ -0,0 +1,31 @@
FROM node:lts-alpine as build-client
COPY . /build/
RUN apk add --no-cache make
RUN make build-client
FROM golang:alpine as build-server
COPY --from=build-client /build /build
RUN apk add --no-cache make
RUN make .bin/gopherhole
FROM alpine
RUN apl add --no-cache ca-certificates
RUN addgroup -g 1000 gopherhole \
&& adduser -H -D -u 1000 gopherhole gopherhole
COPY --chown=gopherhole:gopherhole --from=build-server /build/.bin/gopherhole /opt/gopherhole
USER gopherhole
VOLUME "/data"
ENTRYPOINT /opt/gopherhole

2210
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,15 @@
import { sub } from 'date-fns' import { sub, getUnixTime } from 'date-fns'
import { formatDate } from './metrics'; import { API_HOST } from './util';
export const getLogs = async function ({ export const getLogs = async function ({
start = sub(new Date(), { hours: 12 }), start = sub(new Date(), { hours: 12 }),
end = new Date(), end = new Date(),
page = 0,
filter = "" filter = ""
} = {}) { } = {}) {
try { try {
const data = await fetch(`http://localhost:8080/api/v1/metrics/log?filter=${filter}`, { const data = await fetch(API_HOST(`metrics/log?filter=${filter}&start=${getUnixTime(start)}&end=${getUnixTime(end)}&page=${page}`), {
"method": "GET", "method": "GET",
"headers": { "Accept": "application/json" } "headers": { "Accept": "application/json" }
}); });

View File

@ -1,5 +1,6 @@
import { sub, format } from 'date-fns'; import { getUnixTime, sub, format } from 'date-fns';
import { readable } from "svelte/store"; import { readable } from "svelte/store";
import { API_HOST } from './util';
export const SearchDateFormatStr = "yyyy-MM-dd HH:mm:ss.SSS"; export const SearchDateFormatStr = "yyyy-MM-dd HH:mm:ss.SSS";
export const formatDateForSearch = date => format(date, SearchDateFormatStr); export const formatDateForSearch = date => format(date, SearchDateFormatStr);
@ -26,9 +27,9 @@ export const fetchMetrics = async function ({
} = {}) { } = {}) {
try { try {
const response = await fetch( const response = await fetch(
`http://localhost:8080/api/v1/metrics/stats?start=${formatDateForSearch( API_HOST(`metrics/stats?start=${getUnixTime(
start start
)}&end=${formatDateForSearch(end)}&key=${key}&interval=${interval}`, )}&end=${getUnixTime(end)}&key=${key}&interval=${interval}`),
{ {
method: "GET", method: "GET",
headers: { Accept: "application/json" } headers: { Accept: "application/json" }

View File

@ -5,7 +5,7 @@ interface APIResponse<T> {
} }
export const API_HOST = `http://localhost:8080/api/v1`; export const API_HOST = (url = "") => `http://localhost:8000/api/v1/${url}`;
export const apiCall = async function<T>(url: string, method: string = 'GET'): Promise<APIResponse<T>> { export const apiCall = async function<T>(url: string, method: string = 'GET'): Promise<APIResponse<T>> {
try { try {

View File

@ -47,5 +47,8 @@
{row.TotalTimeMs} {row.TotalTimeMs}
</Column> </Column>
</Table> </Table>
{:else}
<p>No Logs yet!</p>
<p><em>TODO:</em> Link to docs on how to point your router at this server</p>
{/if} {/if}
</div> </div>

View File

@ -1,7 +1,7 @@
{ {
"database": "./db.sqlite", "database": "./db.sqlite",
"cache": "in-memory", "cache": "in-memory",
"http-addr": "localhost:8080", "http-addr": "localhost:8000",
"dns-addr": "localhost:5353", "dns-addr": "localhost:5353",
"recursors": ["192.168.1.15:8600", "1.1.1.1", "8.8.8.8"], "recursors": ["192.168.1.15:8600", "1.1.1.1", "8.8.8.8"],
"rules": [ "rules": [

BIN
gopherhole Executable file

Binary file not shown.

26
internal/events.go Normal file
View File

@ -0,0 +1,26 @@
package internal
import "context"
var (
LogUpdated = EventType("LOG_UPDATE")
DNSLog = EventSource("DNS_LOG")
)
type EventType string
type EventSource string
type Event struct {
Type EventType `json:"type"`
Source EventSource `json:"source"`
Payload interface{} `json:"payload"`
}
type EventPublisher interface {
Publish(Event)
}
type EventSubscriber interface {
Subscribe(context.Context) <-chan Event
}

View File

@ -1,12 +1,8 @@
package internal package internal
import ( import (
"encoding/json"
"errors"
"fmt"
"log" "log"
"net/http" "net/http"
"strconv"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -23,6 +19,8 @@ var upgrader = websocket.Upgrader{
type adminHandler struct { type adminHandler struct {
Cache Cache
Storage Storage
EventSubscriber
EventPublisher
*RuleEngine *RuleEngine
h http.Handler h http.Handler
} }
@ -57,17 +55,23 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
r.Get("/metrics/log", RestHandler(a.getLog).ToHF()) r.Get("/metrics/log", RestHandler(a.getLog).ToHF())
r.Get("/metrics/stats", RestHandler(a.getStats).ToHF()) r.Get("/metrics/stats", RestHandler(a.getStats).ToHF())
r.Get("/rules", RestHandler(a.getRules).ToHF())
r.Put("/rules", RestHandler(a.createRule).ToHF()) r.Put("/rules", RestHandler(a.createRule).ToHF())
r.Get("/rules", RestHandler(a.getRules).ToHF())
r.Get("/rules/{id:[0-9]+}", RestHandler(a.getRule).ToHF())
r.Post("/rules/{id:[0-9]+}", RestHandler(a.updateRule).ToHF())
r.Delete("/rules/{id:[0-9]+}", RestHandler(a.deleteRule).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("/recursors", RestHandler(a.addRecursor).ToHF())
r.Get("/recursors", RestHandler(a.getRecursors).ToHF())
r.Get("/recursors/{id:[0-9]+}", RestHandler(a.getRecursor).ToHF())
r.Delete("/recursor/{id:[0-9]+}", RestHandler(a.deleteRecursor).ToHF())
// r.Put("/rules/lists", a.addRulelist) // r.Put("/rules/lists", a.addRulelist)
// r.Get("/rules/lists", a.getRuleLists) // r.Get("/rules/lists", a.getRuleLists)
// r.Delete("/rules/lists/{id}", a.deleteRuleList)
// r.Post("/rules/lists/reload/{id}", a.reloadRuleLists) // r.Post("/rules/lists/reload/{id}", a.reloadRuleLists)
// r.Delete("/rules/lists/{id}", a.deleteRuleList)
// r.Get("/updates", RestHandler(a.getStats).ToHF())
// r.Delete("/cache/purgeall", RestHandler(a.purgeAll).ToHF()) // r.Delete("/cache/purgeall", RestHandler(a.purgeAll).ToHF())
// r.Delete("/cache/purge", a.purgeKey) // r.Delete("/cache/purge", a.purgeKey)
// r.Get("/cache", a.getCacheContents) // r.Get("/cache", a.getCacheContents)
@ -75,7 +79,6 @@ func NewAdminHandler(c Cache, s Storage, re *RuleEngine) http.Handler {
}) })
return a return a
} }
func (a *adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (a *adminHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -91,319 +94,7 @@ func (a *adminHandler) signal(w http.ResponseWriter, r *http.Request) {
defer c.Close() defer c.Close()
for { for {
// send any updates that come through to the client // send any updates that come through to the client
} }
} }
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)
if err != nil {
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: false,
Payload: "Invalid rule ID",
},
}, nil
}
if err := a.Storage.DeleteRule(ruleId); err != nil {
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: false,
Payload: err.Error(),
},
}, nil
}
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: true,
Payload: ruleId,
},
}, nil
}
func (a *adminHandler) createRule(r *http.Request) (*RestResponse, error) {
var rr RuleRow
if err := json.NewDecoder(r.Body).Decode(&rr); err != nil {
return &RestResponse{
Status: http.StatusUnprocessableEntity,
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: false,
Payload: err.Error(),
},
}, nil
}
if err := a.Storage.AddRule(rr); err != nil {
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: false,
Payload: err.Error(),
},
}, nil
}
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: true,
},
}, nil
}
func (a *adminHandler) getRules(r *http.Request) (*RestResponse, error) {
results, err := a.Storage.GetRules()
if err != nil {
return nil, err
}
if len(results) <= 0 {
results = []RuleRow{}
}
return &RestResponse{
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: true,
Payload: results,
},
}, nil
}
func (a *adminHandler) getStats(r *http.Request) (*RestResponse, error) {
q := r.URL.Query()
startFilter := q.Get("start")
endFilter := q.Get("end")
key := q.Get("key")
intervalSecondsStr := q.Get("interval")
var err error
start := time.Now().UTC().Add(time.Hour * -86400)
end := time.Now().UTC()
if startFilter != "" {
if start, err = time.Parse(ISO8601, startFilter); err != nil {
return nil, err
}
}
if endFilter != "" {
if end, err = time.Parse(ISO8601, endFilter); err != nil {
return nil, err
}
}
lai := LogAggregateInput{
Start: start,
End: end,
Column: key,
}
if intervalSecondsStr != "" {
if lai.IntervalSeconds, err = strconv.Atoi(intervalSecondsStr); err != nil {
return nil, errors.New("interval query param must be a valid whole number")
}
}
la, err := a.Storage.GetLogAggregate(lai)
if err != nil {
return nil, err
}
return &RestResponse{
Status: http.StatusOK,
Payload: struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}{
Success: true,
Payload: la,
},
}, nil
}
func (a *adminHandler) getLog(r *http.Request) (*RestResponse, error) {
q := r.URL.Query()
startFilter := q.Get("start")
endFilter := q.Get("end")
// filter := q.Get("filter")
pageStr := q.Get("page")
var err error
var page int
start := time.Now().UTC().Add(time.Hour * -86400)
end := time.Now().UTC()
if startFilter != "" {
if start, err = time.Parse(ISO8601, startFilter); err != nil {
return nil, err
}
}
if endFilter != "" {
if end, err = time.Parse(ISO8601, endFilter); err != nil {
return nil, err
}
}
if pageStr != "" {
page, _ = strconv.Atoi(pageStr)
}
ql, err := a.Storage.GetLog(GetLogInput{
Start: start,
End: end,
Limit: 250,
Page: page,
})
if err != nil {
return nil, err
}
return &RestResponse{
Status: http.StatusOK,
Payload: struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}{
Success: true,
Payload: ql,
},
}, nil
}
type RestResponse struct {
Status int
Headers http.Header
Payload struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}
}
func (rr *RestResponse) Write(w http.ResponseWriter) error {
if rr.Status != 0 && rr.Status != 200 {
w.WriteHeader(rr.Status)
}
for k, v := range rr.Headers {
for _, ve := range v {
w.Header().Add(k, ve)
}
}
e := json.NewEncoder(w)
e.SetIndent("\n", "\t")
if err := e.Encode(rr.Payload); err != nil {
return fmt.Errorf("could not serialize struct for http response: %w", err)
}
return nil
}
type RestHandler func(request *http.Request) (*RestResponse, error)
func (rh RestHandler) ToHF() http.HandlerFunc {
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 {
return &RestResponse{
Status: http.StatusInternalServerError,
Payload: struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}{
Success: false,
Payload: e.Error(),
},
}
}
func (r RestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
response, err := r(req)
if err != nil {
response = r.Error(err)
}
if err := response.Write(w); err != nil {
log.Printf("Error occurred handling rest response: %v", err)
}
}

View File

@ -0,0 +1,42 @@
package internal
import (
"encoding/json"
"net/http"
)
func (a *adminHandler) addRecursor(r *http.Request) (*RestResponse, error) {
var recursorHttpInput RecursorRow
if err := json.NewDecoder(r.Body).Decode(&recursorHttpInput); err != nil {
return ErrorResponse(http.StatusUnprocessableEntity, err), nil
}
ipAddr, port, ok := recursorHttpInput.ValidIp()
if !ok {
return BasicResponse(false, "The ip address provided is invalid."), nil
}
if err := a.Storage.AddRecursors(ipAddr, port, recursorHttpInput.TimeoutMs, recursorHttpInput.Weight); err != nil {
return nil, err
}
return BasicResponse(true, recursorHttpInput), nil
}
func (a *adminHandler) getRecursors(r *http.Request) (*RestResponse, error) {
recursors, err := a.Storage.GetRecursors()
if err != nil {
return nil, err
}
return BasicResponse(true, recursors), nil
}
func (a *adminHandler) getRecursor(r *http.Request) (*RestResponse, error) {
return BasicResponse(false, "Nothing here yet"), nil
}
func (a *adminHandler) deleteRecursor(r *http.Request) (*RestResponse, error) {
return BasicResponse(false, "Nothing here yet"), nil
}

89
internal/http_rules.go Normal file
View File

@ -0,0 +1,89 @@
package internal
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi"
)
func (a *adminHandler) createRule(r *http.Request) (*RestResponse, error) {
var rr RuleRow
if err := json.NewDecoder(r.Body).Decode(&rr); err != nil {
return ErrorResponse(http.StatusUnprocessableEntity, err), nil
}
if err := a.Storage.AddRule(rr); err != nil {
return nil, err
}
return BasicResponse(true, nil), nil
}
func (a *adminHandler) getRule(r *http.Request) (*RestResponse, error) {
ruleIdParam := chi.URLParam(r, "id")
ruleId, err := strconv.Atoi(ruleIdParam)
if err != nil {
return BasicResponse(false, "`id` must be a valid integer."), nil
}
results, err := a.Storage.GetRule(ruleId)
if err != nil {
return nil, err
}
return BasicResponse(true, results), nil
}
func (a *adminHandler) getRules(r *http.Request) (*RestResponse, error) {
results, err := a.Storage.GetRules()
if err != nil {
return nil, err
}
if len(results) <= 0 {
results = []RuleRow{}
}
return BasicResponse(true, results), nil
}
func (a *adminHandler) deleteRule(r *http.Request) (*RestResponse, error) {
ruleIdParam := chi.URLParam(r, "id")
ruleId, err := strconv.Atoi(ruleIdParam)
if err != nil {
return BasicResponse(false, "`id` must be a valid integer"), nil
}
if err := a.Storage.DeleteRule(ruleId); err != nil {
return nil, err
}
rule, err := a.Storage.GetRule(ruleId)
if err != nil {
return nil, err
}
return BasicResponse(true, rule), nil
}
func (a *adminHandler) updateRule(r *http.Request) (*RestResponse, error) {
var rr RuleRow
if err := json.NewDecoder(r.Body).Decode(&rr); err != nil {
return ErrorResponse(http.StatusUnprocessableEntity, err), nil
}
if err := a.UpdateRule(rr.ID, rr); err != nil {
return nil, err
}
rule, err := a.GetRule(rr.ID)
if err != nil {
return nil, err
}
return BasicResponse(true, rule), nil
}

117
internal/http_stats.go Normal file
View File

@ -0,0 +1,117 @@
package internal
import (
"net/http"
"strconv"
"time"
)
func (a *adminHandler) getStats(r *http.Request) (*RestResponse, error) {
q := r.URL.Query()
startFilter := q.Get("start")
endFilter := q.Get("end")
key := q.Get("key")
intervalSecondsStr := q.Get("interval")
var err error
startTime := time.Now().Add(time.Hour * -86400)
endTime := time.Now()
if startFilter != "" {
var startUnixTime int64
if startUnixTime, err = strconv.ParseInt(startFilter, 10, strconv.IntSize); err != nil {
return BasicResponse(false, "start: must be a valid unix timestamp"), nil
}
startTime = time.Unix(startUnixTime, 0)
}
if endFilter != "" {
var endUnixTime int64
if endUnixTime, err = strconv.ParseInt(endFilter, 10, strconv.IntSize); err != nil {
return BasicResponse(false, "end: must be a valid unix timestamp"), nil
}
endTime = time.Unix(endUnixTime, 0)
}
lai := LogAggregateInput{
Start: startTime,
End: endTime,
Column: key,
}
if intervalSecondsStr != "" {
if lai.IntervalSeconds, err = strconv.Atoi(intervalSecondsStr); err != nil {
return BasicResponse(false, "interval query param must be a valid whole number"), nil
}
}
la, err := a.Storage.GetLogAggregate(lai)
if err != nil {
return nil, err
}
return BasicResponse(true, la), nil
}
type LogFilter struct {
Expression string
}
func (a *adminHandler) getLog(r *http.Request) (*RestResponse, error) {
q := r.URL.Query()
startFilter := q.Get("start")
endFilter := q.Get("end")
// filter := LogFilter{Expression: q.Get("filter")}
pageStr := q.Get("page")
var err error
var page int
startTime := time.Now().Add(time.Hour * -86400)
endTime := time.Now()
if startFilter != "" {
var startUnixTime int64
if startUnixTime, err = strconv.ParseInt(startFilter, 10, strconv.IntSize); err != nil {
return BasicResponse(false, "start: must be a valid unix timestamp"), nil
}
startTime = time.Unix(startUnixTime, 0)
}
if endFilter != "" {
var endUnixTime int64
if endUnixTime, err = strconv.ParseInt(endFilter, 10, strconv.IntSize); err != nil {
return BasicResponse(false, "end: must be a valid unix timestamp"), nil
}
endTime = time.Unix(endUnixTime, 0)
}
if pageStr != "" {
if page, err = strconv.Atoi(pageStr); err != nil {
return BasicResponse(false, "page: must be a valid integer"), nil
}
}
gli := GetLogInput{
// Filter: filter,
Start: startTime,
End: endTime,
Limit: 250,
Page: page,
}
// if err := gli.Validate(); err != nil {
// return BasicResponse(false, err.Error()), nil
// }
ql, err := a.Storage.GetLog(gli)
if err != nil {
return nil, err
}
return BasicResponse(true, ql), nil
}

95
internal/rest_helpers.go Normal file
View File

@ -0,0 +1,95 @@
package internal
import (
"encoding/json"
"fmt"
"log"
"net/http"
)
type RestResponse struct {
Status int
Headers http.Header
Payload struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}
}
func BasicResponse(success bool, model interface{}) *RestResponse {
return &RestResponse{
Status: http.StatusOK,
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{Success: success, Payload: model},
}
}
func ErrorResponse(status int, err error) *RestResponse {
return &RestResponse{
Status: status,
Payload: struct {
Success bool "json:\"success\""
Payload interface{} "json:\"payload\""
}{
Success: false,
Payload: err.Error(),
},
}
}
func (rr *RestResponse) Write(w http.ResponseWriter) error {
if rr.Status != 0 && rr.Status != 200 {
w.WriteHeader(rr.Status)
}
for k, v := range rr.Headers {
for _, ve := range v {
w.Header().Add(k, ve)
}
}
e := json.NewEncoder(w)
e.SetIndent("\n", "\t")
if err := e.Encode(rr.Payload); err != nil {
return fmt.Errorf("could not serialize struct for http response: %w", err)
}
return nil
}
type RestHandler func(request *http.Request) (*RestResponse, error)
func (rh RestHandler) ToHF() http.HandlerFunc {
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 {
return &RestResponse{
Status: http.StatusInternalServerError,
Payload: struct {
Success bool `json:"success"`
Payload interface{} `json:"payload"`
}{
Success: false,
Payload: e.Error(),
},
}
}
func (r RestHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
response, err := r(req)
if err != nil {
response = r.Error(err)
}
if err := response.Write(w); err != nil {
log.Printf("Error occurred handling rest response: %v", err)
}
}

View File

@ -4,6 +4,7 @@ import (
"database/sql" "database/sql"
"fmt" "fmt"
"io" "io"
"log"
"net" "net"
"strconv" "strconv"
"strings" "strings"
@ -18,13 +19,16 @@ type Storage interface {
io.Closer io.Closer
Open() error Open() error
AddRecursors(net.IP, int, int, int) error AddRecursors(net.IP, int, int, int) error
DeleteRecursors(int) error
GetRecursors() ([]RecursorRow, error) GetRecursors() ([]RecursorRow, error)
// UpdateRule(RuleRow) error UpdateRecursor(int, RecursorRow) error
DeleteRecursors(int) error
AddRule(RuleRow) error AddRule(RuleRow) error
GetRule(int) (RuleRow, error) GetRule(int) (RuleRow, error)
GetRules() ([]RuleRow, error) GetRules() ([]RuleRow, error)
UpdateRule(int, RuleRow) error
DeleteRule(int) error DeleteRule(int) error
Log(QueryLog) error Log(QueryLog) error
GetLog(GetLogInput) ([]QueryLog, error) GetLog(GetLogInput) ([]QueryLog, error)
GetLogAggregate(LogAggregateInput) ([]LogAggregateDataPoint, error) GetLogAggregate(LogAggregateInput) ([]LogAggregateDataPoint, error)
@ -97,9 +101,17 @@ func (rr RecursorRow) ValidIp() (net.IP, int, bool) {
return parsedIp, parsedPort, true return parsedIp, parsedPort, true
} }
func (ss *Sqlite) UpdateRecursor(id int, in RecursorRow) error {
sql := `UPDATE recursors SET ipAddress = ?, timeoutMs = ?, weight = ? WHERE id = ?;`
if _, err := ss.Exec(sql, in.IpAddress, in.TimeoutMs, in.Weight); err != nil {
return fmt.Errorf("could not update recursor: %w", err)
}
return nil
}
func (ss *Sqlite) AddRecursors(ip net.IP, port, timeout, weight int) error { func (ss *Sqlite) AddRecursors(ip net.IP, port, timeout, weight int) error {
sql := `INSERT INTO recursors (ipAddress, timeoutMs, weight) VALUES (?, ?, ?);` sql := `INSERT INTO recursors (ipAddress, timeoutMs, weight) VALUES (?, ?, ?);`
if _, err := ss.Exec(sql, fmt.Sprintf("%s:%d", ip.String(), port), timeout, weight); err != nil { if _, err := ss.Exec(sql, fmt.Sprintf("%s:%d", ip.String(), port), timeout, weight); err != nil {
return fmt.Errorf("could not insert recursor: %w", err) return fmt.Errorf("could not insert recursor: %w", err)
} }
@ -123,6 +135,24 @@ type RuleRow struct {
Rule Rule
} }
func (ss *Sqlite) UpdateRule(id int, in RuleRow) error {
sql := `UPDATE rules SET
name = ?,
expression = ?,
answerType = ?,
answerValue = ?,
ttl = ?,
weight = ?,
enabled = ?
WHERE id = ?;`
if _, err := ss.Exec(sql, in.Name, in.Value, in.Answer.Type, in.Answer.Value, in.TTL, in.Weight, in.Enabled); err != nil {
return fmt.Errorf("could not update rule with id: %v", id)
}
return nil
}
func (ss *Sqlite) AddRule(rr RuleRow) error { 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, ?);` VALUES (?, ? , ?, ?, ?, ?, 1, ?);`
@ -171,7 +201,11 @@ func (ss *Sqlite) GetRules() ([]RuleRow, error) {
} }
defer rows.Close() defer rows.Close()
var results []RuleRow if rerr := rows.Err(); rerr != nil {
return nil, err
}
results := []RuleRow{}
for rows.Next() { for rows.Next() {
var rule RuleRow var rule RuleRow
var createdTime string var createdTime string
@ -207,36 +241,43 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
} }
if in.Start.IsZero() { if in.Start.IsZero() {
in.Start = time.Now().UTC().Add(time.Hour * -86400) in.Start = time.Now().Add(time.Hour * -86400)
} }
if in.End.IsZero() { if in.End.IsZero() {
in.End = time.Now().UTC() in.End = time.Now()
} }
sql := ` sql := `
SELECT SELECT
started, clientIp, protocol, domain, totalTimeMs, error, recurseRoundTripTimeMs, recurseUpstreamIp, status started, clientIp, protocol, domain, totalTimeMs,
error, recurseRoundTripTimeMs, recurseUpstreamIp, status
FROM FROM
log log
WHERE WHERE
id > ? AND started > ? AND started < ? id > ?
AND strftime('%s', started) > strftime('%s', ?)
AND strftime('%s', started) < strftime('%s', ?)
ORDER BY started DESC ORDER BY started DESC
LIMIT ?; LIMIT ?;
` `
rows, err := ss.DB.Query(sql, in.Page*in.Limit, in.Start.Format(ISO8601), in.End.Format(ISO8601), in.Limit) rows, err := ss.DB.Query(sql, in.Page*in.Limit, in.Start.UTC().Format(ISO8601), in.End.UTC().Format(ISO8601), in.Limit)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("issue with GetLog sql query: %w", err)
} }
defer rows.Close() defer rows.Close()
var ql []QueryLog if rerr := rows.Err(); rerr != nil {
return nil, fmt.Errorf("issue with rows object: %w", rerr)
}
ql := []QueryLog{}
for rows.Next() { for rows.Next() {
var q QueryLog var q QueryLog
var started string var started string
rows.Scan( if err := rows.Scan(
&started, &started,
&q.ClientIP, &q.ClientIP,
&q.Protocol, &q.Protocol,
@ -246,14 +287,18 @@ func (ss *Sqlite) GetLog(in GetLogInput) ([]QueryLog, error) {
&q.RecurseRoundTripTimeMs, &q.RecurseRoundTripTimeMs,
&q.RecurseUpstreamIP, &q.RecurseUpstreamIP,
&q.Status, &q.Status,
) ); err != nil {
return nil, fmt.Errorf("issues scanning rows: %w", err)
}
if q.Started, err = time.Parse(ISO8601, started); err != nil { if q.Started, err = time.Parse(ISO8601, started); err != nil {
return nil, fmt.Errorf("could not parse time '%s': %v", started, err) return nil, fmt.Errorf("could not parse time '%s': %w", started, err)
} }
log.Printf("%+v", q)
ql = append(ql, q) ql = append(ql, q)
} }
log.Printf("%+v", ql)
return ql, nil return ql, nil
} }
@ -321,6 +366,10 @@ func (ss *Sqlite) GetLogAggregate(la LogAggregateInput) ([]LogAggregateDataPoint
} }
defer rows.Close() defer rows.Close()
if err := rows.Err(); err != nil {
return nil, err
}
var results []LogAggregateDataPoint var results []LogAggregateDataPoint
for rows.Next() { for rows.Next() {
var ladp LogAggregateDataPoint var ladp LogAggregateDataPoint
@ -367,7 +416,7 @@ func (ss *Sqlite) Log(ql QueryLog) error {
} }
func (ss *Sqlite) Open() error { func (ss *Sqlite) Open() error {
db, err := sql.Open("sqlite3", fmt.Sprintf("%s?cache=shared", ss.Path)) db, err := sql.Open("sqlite3", fmt.Sprintf("%s?cache=shared&_journal=WAL", ss.Path))
if err != nil { if err != nil {
return fmt.Errorf("could not open db: %w", err) return fmt.Errorf("could not open db: %w", err)
} }

View File

@ -16,6 +16,9 @@ import (
var ( var (
configFilePath = flag.String("config", "./config.json", "Config file") configFilePath = flag.String("config", "./config.json", "Config file")
// 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() { func main() {
@ -25,6 +28,8 @@ func main() {
if err := LoadStartupConfig(&conf, *configFilePath); err != nil { if err := LoadStartupConfig(&conf, *configFilePath); err != nil {
log.Fatalf("%+v", err) log.Fatalf("%+v", err)
} }
// conf.HTTPAddr = *httpAddr
// conf.DNSAddr = *dnsAddr
log.Printf("%+v", conf) log.Printf("%+v", conf)
store := &internal.Sqlite{ store := &internal.Sqlite{
Path: conf.DatabaseURL, Path: conf.DatabaseURL,

View File

@ -8,7 +8,7 @@ client/node_modules:
cd ./client && npm install cd ./client && npm install
clean: clean:
@rm -rf .bin/gopherhole @rm -rf .bin/gopherhole .bin/config.json
clobber: clobber:
@rm -rf .bin ./client/node_modules @rm -rf .bin ./client/node_modules